diff --git a/.circleci/README.md b/.circleci/README.md deleted file mode 100644 index 862468906..000000000 --- a/.circleci/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Publishing and Releasing - -## Publishing Images - -Images are published based on the logic in [should_publish.sh](./should_publish.sh), and the regex provided in `PUBLISHABLE_TAGS` and `PUBLISHABLE_BRANCHES` environment variables in the CircleCI [config](./config.yml). - -Currently images are published in the following conditions: - -- any commit to branches named `main`, `hotfix.*`, or `alpha.*` -- any branch tagged with [semver](https://semver.org/) `major.minor.patch` (regex: `^[0-9]+\.[0-9]+\.[0-9]+$`) - -## Creating a release - -The easiest way to create a new release is to [Create a New Release](https://github.com/specklesystems/speckle-server/releases/new) on Github, and in the 'Select A Tag' dropdown create a new tag with the appropriate [semver](https://semver.org/) increment. - -Ideally the target branch should be `main`. diff --git a/.circleci/build.sh b/.circleci/build.sh deleted file mode 100755 index 1558c9bf5..000000000 --- a/.circleci/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# enables building the test-deployment container with the same script -# defaults to packages for minimal intervention in the ci config -FOLDER="${FOLDER:-packages}" - -SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" -# shellcheck disable=SC1090,SC1091 -source "${SCRIPT_DIR}/common.sh" - -echo "Building image: ${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" - -export DOCKER_BUILDKIT=1 - -docker build --build-arg SPECKLE_SERVER_VERSION="${IMAGE_VERSION_TAG}" --tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" --file "${FOLDER}/${SPECKLE_SERVER_PACKAGE}/Dockerfile" . - -echo "🐳 Logging into Docker" -echo "${DOCKER_REG_PASS}" | docker login -u "${DOCKER_REG_USER}" --password-stdin "${DOCKER_REG_URL}" - -echo "⏫ Pushing image: '${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}'" -docker push "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" diff --git a/.circleci/build_publish_fe2_sourcemaps.sh b/.circleci/build_publish_fe2_sourcemaps.sh deleted file mode 100755 index fa7e37693..000000000 --- a/.circleci/build_publish_fe2_sourcemaps.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -GIT_ROOT="$(git rev-parse --show-toplevel)" -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -# shellcheck disable=SC1090,SC1091 -source "${SCRIPT_DIR}/common.sh" - -FE2_DIR_PATH="${FE2_DIR_PATH:-"packages/frontend-2"}" -FE2_DATADOG_SERVICE="${FE2_DATADOG_SERVICE:-"web-app-2"}" -DATADOG_SITE="${DATADOG_SITE:-"datadoghq.eu"}" - -if [[ -z "${DATADOG_API_KEY}" ]]; then - echo "DATADOG_API_KEY is not set" - exit 1 -fi - -# Build same prod docker image just w/ sourcemaps enabled -export DOCKER_BUILDKIT=1 -docker build --build-arg BUILD_SOURCEMAPS=true --build-arg SPECKLE_SERVER_VERSION="${IMAGE_VERSION_TAG}" --tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}-sourcemaps" --file "${FE2_DIR_PATH}/Dockerfile" . -container_id=$(docker create "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}-sourcemaps") - -# Clean target location and copy sourcemaps into it -rm -rf "${GIT_ROOT}/${FE2_DIR_PATH}/.output" -docker cp "$container_id":/speckle-server "${GIT_ROOT}/${FE2_DIR_PATH}/.output" -docker rm "$container_id" - -# Publish sourcemaps -pushd "${GIT_ROOT}/${FE2_DIR_PATH}" -DATADOG_SITE="${DATADOG_SITE}" npx --yes @datadog/datadog-ci sourcemaps upload ./.output/public/_nuxt \ ---service="${FE2_DATADOG_SERVICE}" \ ---release-version="${IMAGE_VERSION_TAG}" \ ---minified-path-prefix=/_nuxt -popd - -# Clean up -rm -rf "${GIT_ROOT}/${FE2_DIR_PATH}/.output" diff --git a/.circleci/check_version.py b/.circleci/check_version.py deleted file mode 100755 index 517f5d727..000000000 --- a/.circleci/check_version.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/python3 -import sys -from typing import Optional -from dataclasses import dataclass - - -@dataclass -class Version: - major: int - minor: int - patch: int - pre_release_tag: Optional[str] = None - build_number: Optional[int] = None - - @property - def pre_release_priority(self) -> int: - if self.pre_release_tag == "alpha": - return 1 - if self.pre_release_tag == "beta": - return 2 - return 10 - - @staticmethod - def parse_version_slug(version_slug: str) -> "Version": - members = version_slug.split(".") - assert 3 <= len(members) <= 4 - if len(members) == 3: - major, minor, patch = members - return Version(int(major), int(minor), int(patch)) - - else: - major, minor, patch_and_pre, build = members - patch, pre = patch_and_pre.split("-") - return Version(int(major), int(minor), int(patch), pre, int(build)) - - def __gt__(self, other): - if not isinstance(other, Version): - raise ValueError(f"cannot compare with {other}") - - if self.major > other.major: - return True - if self.major < other.major: - return False - - if self.minor > other.minor: - return True - if self.minor < other.minor: - return False - - if self.patch > other.patch: - return True - if self.patch < other.patch: - return False - - if self.pre_release_tag == other.pre_release_tag: - if self.build_number > other.build_number: - return True - if self.build_number < other.build_number: - return False - - if self.pre_release_priority > other.pre_release_priority: - return True - if self.pre_release_priority < other.pre_release_priority: - return False - - return True - - -if __name__ == "__main__": - print("\nStarting version compare\n") - args = sys.argv[1:] - assert len(args) == 2 - - current_version_slug, target_version_slug = args - - print( - f"comparing current version {current_version_slug} with target {target_version_slug}" - ) - - current_version = Version.parse_version_slug(current_version_slug) - target_version = Version.parse_version_slug(target_version_slug) - - if target_version > current_version: - print("target version is newer\n") - exit(0) - - print("current version is newer\n") - exit(1) diff --git a/.circleci/common.sh b/.circleci/common.sh deleted file mode 100755 index 9d7b119a9..000000000 --- a/.circleci/common.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -# shellcheck disable=SC2034 -DOCKER_IMAGE_TAG="speckle/speckle-${SPECKLE_SERVER_PACKAGE}" -IMAGE_VERSION_TAG="${IMAGE_VERSION_TAG:-${CIRCLE_SHA1}}" -# shellcheck disable=SC2068,SC2046 -LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags) | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)" # get the last release tag. FIXME: Fails if a commit is tagged with more than one tag: https://stackoverflow.com/questions/8089002/git-describe-with-two-tags-on-the-same-commit/56039163#56039163 -# shellcheck disable=SC2034 -NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')" -# shellcheck disable=SC2034 -BRANCH_NAME_TRUNCATED="$(echo "${CIRCLE_BRANCH}" | cut -c -28 | sed 's/[^a-zA-Z0-9.-]/-/g')" # Kubernetes has a 63 character limit, so ensuring the branch name will be short enough. -# shellcheck disable=SC2034 -COMMIT_SHA1_TRUNCATED="$(echo "${CIRCLE_SHA1}" | cut -c -7)" diff --git a/.circleci/config.yml b/.circleci/config.yml index 47eed7ce6..ade0df78b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,41 +2,18 @@ version: 2.1 orbs: snyk: snyk/snyk@2.0.3 - codecov: codecov/codecov@5.0.3 aliases: - - &docker-base-image - docker: - - image: cimg/base:2024.02 - - &docker-node-image docker: - image: cimg/node:22.6.0 - - &docker-node-image-w-browsers - docker: - - image: cimg/node:22.6.0-browsers - - &yarn run: name: Install Dependencies command: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn - - &filters-allow-all - tags: - # run tests for any commit on any branch, including any tags - only: /.*/ - - - &ignored - branches: - ignore: /.*/ - tags: - ignore: /.*/ - - - &branches-special - - main - - /^hotfix.*$/ - - /^testing\d*$/ + - &work-dir /tmp/ci workflows: test-build: @@ -50,748 +27,7 @@ workflows: - main - hotfix* - - get-version: - filters: *filters-allow-all - - - docker-build-postgres-container: - context: &build-context - - github-readonly-public-repos - - docker-hub - filters: *filters-allow-all - requires: - - get-version - - - test-server-multiregion: - context: - - speckle-server-licensing - - stripe-integration - - speckle-server-codecov - filters: *filters-allow-all - requires: - - docker-build-postgres-container - - - test-server: - context: - - speckle-server-licensing - - stripe-integration - - speckle-server-codecov - filters: *ignored - requires: - - docker-build-postgres-container - - - test-server-no-ff: - filters: *ignored - requires: - - docker-build-postgres-container - - - test-frontend-2: - filters: *ignored - - - test-viewer: - filters: *ignored - - - test-objectsender: - filters: *ignored - - - test-shared: - filters: *ignored - - - test-preview-service: - filters: *ignored - - - test-ui-components: - filters: *ignored - - - ui-components-chromatic: - context: - - chromatic-ui-components - filters: *ignored - - - deployment-testing-approval: - type: approval - filters: *ignored - - #FIXME uncomment when nix https://search.nixos.org/packages?channel=24.05&show=tilt&from=0&size=50&sort=relevance&type=packages&query=tilt supports tilt >v0.33.12 which includes docker compose up --wait flag - # - deployment-test-docker-compose: - # filters: &filters-deployment-testing - # tags: - # # run tests for any commit on any branch, including any tags - # only: /.*/ - # requires: - # - get-version - # - deployment-testing-approval - # - docker-build-server - # - docker-build-frontend - # - docker-build-frontend-2 - # - docker-build-previews - # - docker-build-webhooks - # - docker-build-file-imports - # - docker-build-test-container - # - docker-build-monitor-container - # - docker-build-docker-compose-ingress - - - deployment-test-helm-chart: - filters: *ignored - requires: - - get-version - - deployment-testing-approval - - docker-build-server - - docker-build-frontend-2 - - docker-build-previews - - docker-build-webhooks - - docker-build-file-imports - - docker-build-test-container - - docker-build-monitor-container - - - pre-commit: - filters: *ignored - - - lint-and-prettier: - filters: *ignored - - - docker-build-server: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-frontend-2: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-webhooks: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-file-imports: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-previews: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-test-container: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-monitor-container: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-build-docker-compose-ingress: - context: *build-context - filters: *ignored - requires: - - get-version - - - docker-publish-server: - context: &docker-hub-context - - docker-hub - filters: *ignored - requires: - - docker-build-server - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-frontend-2: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-frontend-2 - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-frontend-2-sourcemaps: - context: - - github-readonly-public-repos - - datadog-sourcemaps-publish - filters: *ignored - requires: - - get-version - - - docker-publish-webhooks: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-webhooks - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-file-imports: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-file-imports - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-previews: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-previews - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-test-container: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-test-container - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-postgres-container: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-monitor-container - - get-version - - pre-commit - - test-server - - test-server-no-ff - - test-server-multiregion - - - docker-publish-monitor-container: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-monitor-container - - get-version - - pre-commit - - lint-and-prettier - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - docker-publish-docker-compose-ingress: - context: *docker-hub-context - filters: *ignored - requires: - - docker-build-docker-compose-ingress - - get-version - - pre-commit - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-server - - test-server-no-ff - - test-server-multiregion - - test-preview-service - - - publish-helm-chart: - filters: *ignored - requires: - # - deployment-test-docker-compose #FIXME uncomment when nix https://search.nixos.org/packages?channel=24.05&show=tilt&from=0&size=50&sort=relevance&type=packages&query=tilt supports tilt >v0.33.12 which includes docker compose up --wait flag - - deployment-test-helm-chart - - docker-publish-docker-compose-ingress - - docker-publish-file-imports - - docker-publish-frontend-2 - - docker-publish-monitor-container - - docker-publish-previews - - docker-publish-server - - docker-publish-test-container - - docker-publish-webhooks - - get-version - - - publish-helm-chart-oci: - filters: *ignored - context: - - docker-hub - requires: - # - deployment-test-docker-compose #FIXME uncomment when nix https://search.nixos.org/packages?channel=24.05&show=tilt&from=0&size=50&sort=relevance&type=packages&query=tilt supports tilt >v0.33.12 which includes docker compose up --wait flag - - deployment-test-helm-chart - - docker-publish-docker-compose-ingress - - docker-publish-file-imports - - docker-publish-frontend-2 - - docker-publish-monitor-container - - docker-publish-previews - - docker-publish-server - - docker-publish-test-container - - docker-publish-webhooks - - get-version - - - publish-npm: - filters: *ignored - context: - - npm-registry - requires: - - get-version - - test-server - - test-server-no-ff - - test-server-multiregion - - test-ui-components - - test-frontend-2 - - test-viewer - - test-objectsender - - test-shared - - test-preview-service - - - publish-viewer-sandbox-cloudflare-pages: - filters: *ignored - context: - - cloudflare-pages-edit - requires: - - test-viewer - jobs: - get-version: - <<: *docker-base-image - working_directory: &work-dir /tmp/ci - steps: - - checkout - - run: mkdir -p workspace - - run: - name: set version - command: | - echo "export IMAGE_VERSION_TAG=$(.circleci/get_version.sh)" >> workspace/env-vars - - run: cat workspace/env-vars >> $BASH_ENV - - run: echo "IMAGE_VERSION_TAG=${IMAGE_VERSION_TAG}" - - persist_to_workspace: - root: workspace - paths: - - env-vars - - lint-and-prettier: - <<: *docker-node-image - resource_class: xlarge - working_directory: *work-dir - steps: - - checkout - - run: - name: Install Hardened Dependencies - command: YARN_ENABLE_HARDENED_MODE=1 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Lint everything - command: yarn eslint:projectwide - - run: - name: Run prettier check - command: yarn prettier:check - - run: - name: Check JSON Schema is up to date - command: | - yarn helm:jsonschema:generate - git diff --exit-code - - pre-commit: - parameters: - config_file: - default: ./.pre-commit-config.yaml - description: Optional, path to pre-commit config file. - type: string - docker: - - image: speckle/pre-commit-runner:latest - resource_class: large - working_directory: *work-dir - steps: - - checkout - - restore_cache: - name: Restore pre-commit & Yarn Package cache - keys: - - cache-pre-commit--{{ checksum "<>" }} - - run: - name: Install pre-commit hooks - command: pre-commit install-hooks --config <> - - save_cache: - key: cache-pre-commit--{{ checksum "<>" }} - paths: - - ~/.cache/pre-commit - - run: - name: Run pre-commit - command: pre-commit run --all-files --config <> - - run: - command: git --no-pager diff - name: git diff - when: on_fail - - test-server: &test-server-job - docker: - - image: cimg/node:22.6.0 - - image: cimg/redis:7.2.4 - - image: 'speckle/speckle-postgres' - environment: - POSTGRES_DB: speckle2_test - POSTGRES_PASSWORD: speckle - POSTGRES_USER: speckle - command: -c 'max_connections=1000' - - image: 'minio/minio' - command: server /data --console-address ":9001" - # environment: - - resource_class: large - environment: - NODE_ENV: test - DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test' - PGDATABASE: speckle2_test - POSTGRES_MAX_CONNECTIONS_SERVER: 20 - PGUSER: speckle - SESSION_SECRET: 'keyboard cat' - STRATEGY_LOCAL: 'true' - CANONICAL_URL: 'http://127.0.0.1:3000' - S3_ENDPOINT: 'http://127.0.0.1:9000' - S3_ACCESS_KEY: 'minioadmin' - S3_SECRET_KEY: 'minioadmin' - S3_BUCKET: 'speckle-server' - S3_CREATE_BUCKET: 'true' - REDIS_URL: 'redis://127.0.0.1:6379' - S3_REGION: '' # optional, defaults to 'us-east-1' - FRONTEND_ORIGIN: 'http://127.0.0.1:8081' - ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json' - ENABLE_ALL_FFS: 'true' - RATELIMITER_ENABLED: 'false' - steps: - - checkout - - *yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Wait for dependencies to start - command: 'dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m' - - run: - command: cp .env.test-example .env.test - working_directory: 'packages/server' - - run: - name: 'Run tests' - # Extra formatting to get timestamps on each line in CI (for profiling purposes) - command: | - GREP_FLAG="" - - if [ "$RUN_TESTS_IN_MULTIREGION_MODE" == "true" ]; then - GREP_FLAG="--grep @multiregion" - fi - - yarn test:report $GREP_FLAG --color=always | while IFS= read -r line; do echo -e "$(date +%T.%3N) > $line"; done - working_directory: 'packages/server' - no_output_timeout: 30m - - codecov/upload: - files: packages/server/coverage/lcov.info - - store_test_results: - path: packages/server/reports - - # - store_artifacts: - # path: packages/server/coverage/lcov-report - # destination: package/server/coverage - - test-server-no-ff: - <<: *test-server-job - environment: - NODE_ENV: test - DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test' - PGDATABASE: speckle2_test - POSTGRES_MAX_CONNECTIONS_SERVER: 20 - PGUSER: speckle - SESSION_SECRET: 'keyboard cat' - STRATEGY_LOCAL: 'true' - CANONICAL_URL: 'http://127.0.0.1:3000' - S3_ENDPOINT: 'http://127.0.0.1:9000' - S3_ACCESS_KEY: 'minioadmin' - S3_SECRET_KEY: 'minioadmin' - S3_BUCKET: 'speckle-server' - S3_CREATE_BUCKET: 'true' - REDIS_URL: 'redis://127.0.0.1:6379' - S3_REGION: '' # optional, defaults to 'us-east-1' - FRONTEND_ORIGIN: 'http://127.0.0.1:8081' - ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json' - DISABLE_ALL_FFS: 'true' - RATELIMITER_ENABLED: 'false' - - test-server-multiregion: - <<: *test-server-job - docker: - - image: cimg/node:22.6.0 - - image: cimg/redis:7.2.4 - - image: 'speckle/speckle-postgres' - environment: - POSTGRES_DB: speckle2_test - POSTGRES_PASSWORD: speckle - POSTGRES_USER: speckle - command: -c 'max_connections=1000' -c 'wal_level=logical' - - image: 'speckle/speckle-postgres' - environment: - POSTGRES_DB: speckle2_test - POSTGRES_PASSWORD: speckle - POSTGRES_USER: speckle - command: -c 'max_connections=1000' -c 'port=5433' -c 'wal_level=logical' - - image: 'speckle/speckle-postgres' - environment: - POSTGRES_DB: speckle2_test - POSTGRES_PASSWORD: speckle - POSTGRES_USER: speckle - command: -c 'max_connections=1000' -c 'port=5434' -c 'wal_level=logical' - - image: 'minio/minio' - command: server /data --console-address ":9001" --address "0.0.0.0:9000" - - image: 'minio/minio' - command: server /data --console-address ":9021" --address "0.0.0.0:9020" - - image: 'minio/minio' - command: server /data --console-address ":9041" --address "0.0.0.0:9040" - environment: - # Same as test-server: - NODE_ENV: test - DATABASE_URL: 'postgres://speckle:speckle@127.0.0.1:5432/speckle2_test' - PGDATABASE: speckle2_test - POSTGRES_MAX_CONNECTIONS_SERVER: 50 - PGUSER: speckle - SESSION_SECRET: 'keyboard cat' - STRATEGY_LOCAL: 'true' - CANONICAL_URL: 'http://127.0.0.1:3000' - S3_ENDPOINT: 'http://127.0.0.1:9000' - S3_ACCESS_KEY: 'minioadmin' - S3_SECRET_KEY: 'minioadmin' - S3_BUCKET: 'speckle-server' - S3_CREATE_BUCKET: 'true' - REDIS_URL: 'redis://127.0.0.1:6379' - S3_REGION: '' # optional, defaults to 'us-east-1' - FRONTEND_ORIGIN: 'http://127.0.0.1:8081' - ENCRYPTION_KEYS_PATH: 'test/assets/automate/encryptionKeys.json' - FF_BILLING_INTEGRATION_ENABLED: 'true' - # These are the only different env keys: - MULTI_REGION_CONFIG_PATH: '../../.circleci/multiregion.test-ci.json' - FF_GATEKEEPER_MODULE_ENABLED: 'true' - FF_WORKSPACES_MODULE_ENABLED: 'true' - FF_WORKSPACES_MULTI_REGION_ENABLED: 'true' - FF_MOVE_PROJECT_REGION_ENABLED: 'true' - RUN_TESTS_IN_MULTIREGION_MODE: true - RATELIMITER_ENABLED: 'false' - - test-frontend-2: - <<: *docker-node-image-w-browsers - resource_class: xlarge - steps: - - checkout - - *yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Lint everything - command: yarn lint:ci - working_directory: 'packages/frontend-2' - - test-viewer: - <<: *docker-node-image-w-browsers - resource_class: large - steps: - - checkout - - *yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Lint viewer - command: yarn lint:ci - working_directory: 'packages/viewer' - - run: - name: Run tests - command: yarn test - working_directory: 'packages/viewer' - - run: - name: Lint viewer-sandbox - command: yarn lint:ci - working_directory: 'packages/viewer-sandbox' - - run: - name: Build viewer-sandbox - command: yarn build - working_directory: 'packages/viewer-sandbox' - - test-preview-service: - docker: - - image: cimg/node:22.6.0-browsers - - image: cimg/postgres:16.4@sha256:2e4f1a965bdd9ba77aa6a0a7b93968c07576ba2a8a7cf86d5eb7b31483db1378 - environment: - POSTGRES_DB: preview_service_test - POSTGRES_PASSWORD: preview_service_test - POSTGRES_USER: preview_service_test - resource_class: large - environment: {} - steps: - - checkout - - *yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Lint everything - command: yarn lint:ci - working_directory: 'packages/preview-service' - - run: - name: Copy .env.example to .env - command: | - #!/usr/bin/env bash - cp packages/preview-service/.env.example packages/preview-service/.env - sed -i~ '/^PG_CONNECTION_STRING=/s/=.*/="postgres:\/\/preview_service_test:preview_service_test@127.0.0.1:5432\/preview_service_test"/' packages/preview-service/.env - - run: - name: Run tests - command: yarn test - working_directory: 'packages/preview-service' - - test-shared: - <<: *docker-node-image-w-browsers - resource_class: medium+ - steps: - - checkout - - *yarn - - run: - name: Lint - command: yarn lint:ci - working_directory: 'packages/shared' - - run: - name: Run tests (all FFs) - command: ENABLE_ALL_FFS=1 yarn test:ci - working_directory: 'packages/shared' - - run: - name: Run tests (no FFs) - command: DISABLE_ALL_FFS=1 yarn test:ci - working_directory: 'packages/shared' - - codecov/upload: - files: packages/shared/coverage/coverage-final.json - - run: - name: Build - command: yarn build - working_directory: 'packages/shared' - - run: - name: Ensure ESM import works - command: node ./e2e/testEsm.mjs - working_directory: 'packages/shared' - - run: - name: Ensure CJS require works - command: node ./e2e/testCjs.cjs - working_directory: 'packages/shared' - - test-objectsender: - <<: *docker-node-image-w-browsers - resource_class: large - steps: - - checkout - - *yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Run tests - command: yarn test:ci - working_directory: 'packages/objectsender' - - store_artifacts: - path: 'packages/objectsender/coverage' - - test-ui-components: - <<: *docker-node-image-w-browsers - resource_class: xlarge - steps: - - checkout - - run: - name: Install Dependencies - command: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Lint tailwind theme - command: yarn lint:ci - working_directory: 'packages/tailwind-theme' - - run: - name: Lint ui components - command: yarn lint:ci - working_directory: 'packages/ui-components' - - run: - name: Lint component nuxt package - command: yarn lint:ci - working_directory: 'packages/ui-components-nuxt' - - run: - name: Test via Storybook - command: yarn storybook:test:ci - working_directory: 'packages/ui-components' - - ui-components-chromatic: - <<: *docker-node-image - resource_class: medium+ - steps: - - checkout - - *yarn - - run: - name: Build shared packages - command: yarn build:public - - run: - name: Run chromatic - command: yarn chromatic - working_directory: 'packages/ui-components' - vulnerability-scan: # snyk can undertake most types of scans through GitHub integration # which does not require integration with the CI @@ -807,340 +43,3 @@ jobs: - snyk/scan: additional-arguments: --yarn-workspaces --strict-out-of-sync=false fail-on-issues: false - - deployment-test-docker-compose: - machine: - image: ubuntu-2204:2024.05.1 - docker_layer_caching: true - resource_class: large - working_directory: *work-dir - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - # create the nix folder with permissive write permissions - - run: | - sudo mkdir /nix - sudo chmod 777 /nix - - restore_cache: - name: Restore nix cache - keys: - - nix-{{ checksum "./tests/deployment/docker-compose/docker-compose-shell.nix" }} - - run: - name: Install the nix package manager - command: | - sh <(curl -L https://nixos.org/nix/install) --daemon --yes - echo "source /etc/bashrc" >> "${BASH_ENV}" - - run: - name: Initialize nix shell - command: | - nix-shell --run "echo Here, a nix shell for you" ./tests/deployment/docker-compose/docker-compose-shell.nix - - save_cache: - key: nix-{{ checksum "./tests/deployment/docker-compose/docker-compose-shell.nix" }} - paths: - - /nix - - run: cat workspace/env-vars >> $BASH_ENV - - run: nix-shell --run "LOAD_DOCKER='true' tilt ci --file ./tests/deployment/docker-compose/Tiltfile" ./tests/deployment/helm/docker-compose-shell.nix - - deployment-test-helm-chart: - machine: - image: ubuntu-2204:2024.01.1 - docker_layer_caching: true - resource_class: large - working_directory: *work-dir - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - # create the nix folder with permissive write permissions - - run: | - sudo mkdir /nix - sudo chmod 777 /nix - - restore_cache: - name: Restore nix cache - keys: - - nix-{{ checksum "./tests/deployment/helm/helm-chart-shell.nix" }} - - run: - name: Install the nix package manager - command: | - sh <(curl -L https://nixos.org/nix/install) --daemon --yes - echo "source /etc/bashrc" >> "${BASH_ENV}" - - run: - name: Initialize nix shell - command: | - nix-shell \ - --run "echo Here, a nix shell for you" \ - ./tests/deployment/helm/helm-chart-shell.nix - - save_cache: - key: nix-{{ checksum "./tests/deployment/helm/helm-chart-shell.nix" }} - paths: - - /nix - - run: cat workspace/env-vars >> $BASH_ENV - - run: echo "export KUBECONFIG=$(pwd)/.kube/config" >> "${BASH_ENV}" - - run: echo "${KUBECONFIG}" - - run: - name: Template Speckle Server Helm Chart - command: | - nix-shell \ - --run "helm template speckle-server ./utils/helm/speckle-server" \ - ./tests/deployment/helm/helm-chart-shell.nix - - run: - name: Add 127.0.0.1 domains to /etc/hosts - command: | - sudo tee -a /etc/hosts \<<<'127.0.0.1 speckle.internal' - cat /etc/hosts - - run: - name: Change directory permissions to allow kind to create directories - command: | - mkdir -p "./minio-data" - if [ "$(stat -f "%A" "./minio-data")" != "775" ]; then - echo "🔐 We need 'sudo' to set permissions on minio-data directory to 775" - sudo chmod 775 "./minio-data" - fi - mkdir -p "./postgres-data" - if [ "$(stat -f "%A" "./postgres-data")" != "775" ]; then - echo "🔐 We need 'sudo' to set permissions on postgres-data directory to 775" - sudo chmod 775 "./postgres-data" - fi - - run: - name: Deploy Kubernetes (kind) cluster - command: | - nix-shell \ - --run "ctlptl apply --filename ./tests/deployment/helm/cluster-config.yaml" \ - ./tests/deployment/helmhelm-chart-shell.nix - - run: - name: Deploy Kubernetes resources to cluster - command: | - nix-shell \ - --run "LOAD_DOCKER='true' tilt ci --file ./tests/deployment/helm/Tiltfile --context kind-speckle-server --timeout 10m" \ - ./tests/deployment/helm/helm-chart-shell.nix - - docker-build: &build-job - <<: *docker-base-image - resource_class: medium - working_directory: *work-dir - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - - run: cat workspace/env-vars >> $BASH_ENV - - setup_remote_docker: - version: default - docker_layer_caching: true - - run: - name: Build and Push - command: ./.circleci/build.sh - - docker-build-server: - <<: *build-job - environment: - SPECKLE_SERVER_PACKAGE: server - - docker-build-frontend-2: - <<: *build-job - resource_class: xlarge - environment: - SPECKLE_SERVER_PACKAGE: frontend-2 - - docker-publish-frontend-2-sourcemaps: - <<: *docker-node-image - resource_class: xlarge - working_directory: *work-dir - environment: - SPECKLE_SERVER_PACKAGE: frontend-2 - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - - run: cat workspace/env-vars >> $BASH_ENV - - setup_remote_docker: - version: default - docker_layer_caching: true - - run: - name: Build and Publish sourcemaps - command: ./.circleci/build_publish_fe2_sourcemaps.sh - - docker-build-previews: - <<: *build-job - environment: - SPECKLE_SERVER_PACKAGE: preview-service - - docker-build-webhooks: - <<: *build-job - environment: - SPECKLE_SERVER_PACKAGE: webhook-service - - docker-build-file-imports: - <<: *build-job - environment: - SPECKLE_SERVER_PACKAGE: fileimport-service - - docker-build-test-container: - <<: *build-job - environment: - FOLDER: utils - SPECKLE_SERVER_PACKAGE: test-deployment - - docker-build-postgres-container: - <<: *build-job - environment: - FOLDER: utils - SPECKLE_SERVER_PACKAGE: postgres - - docker-build-monitor-container: - <<: *build-job - environment: - SPECKLE_SERVER_PACKAGE: monitor-deployment - - docker-build-docker-compose-ingress: - <<: *build-job - environment: - FOLDER: utils - SPECKLE_SERVER_PACKAGE: docker-compose-ingress - - docker-publish: &publish-job - <<: *docker-base-image - resource_class: medium - working_directory: *work-dir - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - - run: cat workspace/env-vars >> $BASH_ENV - - setup_remote_docker: - version: default - docker_layer_caching: true - - run: - name: Publish - command: ./.circleci/publish.sh - - docker-publish-server: - <<: *publish-job - environment: - SPECKLE_SERVER_PACKAGE: server - - docker-publish-frontend-2: - <<: *publish-job - environment: - SPECKLE_SERVER_PACKAGE: frontend-2 - - docker-publish-previews: - <<: *publish-job - environment: - SPECKLE_SERVER_PACKAGE: preview-service - - docker-publish-webhooks: - <<: *publish-job - environment: - SPECKLE_SERVER_PACKAGE: webhook-service - - docker-publish-file-imports: - <<: *publish-job - environment: - SPECKLE_SERVER_PACKAGE: fileimport-service - - docker-publish-test-container: - <<: *publish-job - environment: - FOLDER: utils - SPECKLE_SERVER_PACKAGE: test-deployment - - docker-publish-postgres-container: - <<: *publish-job - environment: - FOLDER: utils - SPECKLE_SERVER_PACKAGE: postgres - - docker-publish-monitor-container: - <<: *publish-job - environment: - SPECKLE_SERVER_PACKAGE: monitor-deployment - - docker-publish-docker-compose-ingress: - <<: *publish-job - environment: - FOLDER: utils - SPECKLE_SERVER_PACKAGE: docker-compose-ingress - - publish-npm: - <<: *docker-node-image - working_directory: *work-dir - resource_class: large - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - - run: cat workspace/env-vars >> $BASH_ENV - - *yarn - - run: - name: auth to npm as Speckle - command: | - echo "npmRegistryServer: https://registry.npmjs.org/" >> .yarnrc.yml - echo "npmAuthToken: ${NPM_TOKEN}" >> .yarnrc.yml - - run: - name: try login to npm - command: yarn npm whoami - - run: - name: build public packages - command: yarn workspaces foreach -ptvW --no-private run build - - run: - name: bump all versions - # bump all versions in dependency tree order but not in parallel - command: yarn workspaces foreach -tvW version $IMAGE_VERSION_TAG - - run: - name: publish to npm - command: 'yarn workspaces foreach -pvW --no-private npm publish --access public' - - publish-helm-chart: - docker: - - image: cimg/python:3.12.1 - working_directory: *work-dir - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - - run: cat workspace/env-vars >> $BASH_ENV - - add_ssh_keys: - fingerprints: - - '4d:68:70:66:49:97:ba:8b:8c:55:96:df:3d:be:6e:05' - - run: - name: Publish Helm Chart - command: ./.circleci/publish_helm_chart.sh - - publish-helm-chart-oci: - docker: - - image: speckle/pre-commit-runner:latest - working_directory: *work-dir - steps: - - checkout - - attach_workspace: - at: /tmp/ci/workspace - - run: cat workspace/env-vars >> $BASH_ENV - - run: - name: Publish Helm Chart - command: ./.circleci/publish_helm_chart_oci.sh - - publish-viewer-sandbox-cloudflare-pages: - <<: *docker-node-image - working_directory: *work-dir - resource_class: large - steps: - - checkout - - *yarn - - run: - name: Build public packages - command: yarn build:public - - run: - name: Lint viewer-sandbox - command: yarn lint:ci - working_directory: 'packages/viewer-sandbox' - - run: - name: Build viewer-sandbox - command: yarn build - working_directory: 'packages/viewer-sandbox' - - run: - name: Publish Viewer Sandbox to Cloudflare Pages - command: ./.circleci/publish_cloudflare_pages.sh - environment: - CLOUDFLARE_PAGES_PROJECT_NAME: viewer - VIEWER_SANDBOX_DIR_PATH: packages/viewer-sandbox diff --git a/.circleci/get_version.sh b/.circleci/get_version.sh deleted file mode 100755 index e4c28f24c..000000000 --- a/.circleci/get_version.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -set -eo pipefail - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# shellcheck disable=SC1090,SC1091 -source "${SCRIPT_DIR}/common.sh" - -if [[ "${CIRCLE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "${CIRCLE_TAG}" - exit 0 -fi - -if [[ "${CIRCLE_BRANCH}" == "main" ]]; then - echo "${NEXT_RELEASE}-alpha.${CIRCLE_BUILD_NUM}" - exit 0 -fi - -# if branch name truncated contains an underscore, we should exit -if [[ "${BRANCH_NAME_TRUNCATED}" =~ "_" ]]; then - echo "Branch name contains an underscore, exiting" - exit 1 -fi - -echo "${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${CIRCLE_BUILD_NUM}-${COMMIT_SHA1_TRUNCATED}" -exit 0 diff --git a/.circleci/is_draft.sh b/.circleci/is_draft.sh deleted file mode 100755 index 70a69e726..000000000 --- a/.circleci/is_draft.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -# acknowledgements: https://github.com/vitalinfo/circleci-cancel-draft - -set -euf -o pipefail - -if [[ -z "${CIRCLE_PULL_REQUEST}" ]]; then - echo "FALSE" -fi - -if [[ -z "${GITHUB_TOKEN}" ]]; then - echo "GITHUB_TOKEN is not set" - exit 1 -fi - -PR_NUMBER="${CIRCLE_PULL_REQUEST//[!0-9]/}" -RESPONSE=$(curl --silent \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - -H "Accept: application/vnd.github.v3+json" \ - "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pulls/${PR_NUMBER}" -) - -DRAFT=$(echo "${RESPONSE}" | jq ".draft") -DRAFT_LABEL=$(echo "${RESPONSE}" | jq ".labels | map(select(.name | test(\"Draft\"))) | .[]") - -if [[ ${DRAFT} == 'true' || ${DRAFT_LABEL} ]]; then - echo "TRUE" -else - echo "FALSE" -fi - -exit 0 diff --git a/.circleci/multiregion.test-ci.json b/.circleci/multiregion.test-ci.json deleted file mode 100644 index 78619c2af..000000000 --- a/.circleci/multiregion.test-ci.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "main": { - "postgres": { - "connectionUri": "postgresql://speckle:speckle@127.0.0.1:5432/speckle2_test" - }, - "blobStorage": { - "accessKey": "minioadmin", - "secretKey": "minioadmin", - "bucket": "speckle-server", - "createBucketIfNotExists": true, - "endpoint": "http://127.0.0.1:9000", - "s3Region": "us-east-1" - } - }, - "regions": { - "region1": { - "postgres": { - "connectionUri": "postgresql://speckle:speckle@127.0.0.1:5433/speckle2_test" - }, - "blobStorage": { - "accessKey": "minioadmin", - "secretKey": "minioadmin", - "bucket": "speckle-server", - "createBucketIfNotExists": true, - "endpoint": "http://127.0.0.1:9020", - "s3Region": "us-east-1" - } - }, - "region2": { - "postgres": { - "connectionUri": "postgresql://speckle:speckle@127.0.0.1:5434/speckle2_test" - }, - "blobStorage": { - "accessKey": "minioadmin", - "secretKey": "minioadmin", - "bucket": "speckle-server", - "createBucketIfNotExists": true, - "endpoint": "http://127.0.0.1:9040", - "s3Region": "us-east-1" - } - } - } -} diff --git a/.circleci/publish.sh b/.circleci/publish.sh deleted file mode 100755 index 2c36a1d01..000000000 --- a/.circleci/publish.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# shellcheck disable=SC1090,SC1091 -source "${SCRIPT_DIR}/common.sh" - -echo "Starting tagging & publishing of image: ${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" - -echo "🐳 Logging into Docker" -echo "${DOCKER_REG_PASS}" | docker login -u "${DOCKER_REG_USER}" --password-stdin "${DOCKER_REG_URL}" - -echo "⏬ Pulling image: '${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}'" -docker pull "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" - -if [[ "${IMAGE_VERSION_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-alpha\.[0-9]+)?$ ]]; then - echo "🏷 Tagging and pushing image as '${DOCKER_IMAGE_TAG}:latest'" - docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:latest" - docker push "${DOCKER_IMAGE_TAG}:latest" - - if [[ "${IMAGE_VERSION_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "🏷 Tagging and pushing image as '${DOCKER_IMAGE_TAG}:2'" - docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:2" - docker push "${DOCKER_IMAGE_TAG}:2" - fi -else - BRANCH_TAG="${BRANCH_NAME_TRUNCATED}" - echo "🏷 Tagging and pushing image as '${DOCKER_IMAGE_TAG}:${BRANCH_TAG}'" - docker tag "${DOCKER_IMAGE_TAG}:${IMAGE_VERSION_TAG}" "${DOCKER_IMAGE_TAG}:${BRANCH_TAG}" - docker push "${DOCKER_IMAGE_TAG}:${BRANCH_TAG}" -fi - -echo "✅ Publishing completed." diff --git a/.circleci/publish_cloudflare_pages.sh b/.circleci/publish_cloudflare_pages.sh deleted file mode 100755 index bb7a35d62..000000000 --- a/.circleci/publish_cloudflare_pages.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail - -GIT_ROOT="$(git rev-parse --show-toplevel)" - -CLOUDFLARE_PAGES_PROJECT_NAME="${CLOUDFLARE_PAGES_PROJECT_NAME:-"viewer"}" -VIEWER_SANDBOX_DIR_PATH="${VIEWER_SANDBOX_DIR_PATH:-"packages/viewer-sandbox"}" - -pushd "${GIT_ROOT}/${VIEWER_SANDBOX_DIR_PATH}" -yarn wrangler pages deploy "${GIT_ROOT}/${VIEWER_SANDBOX_DIR_PATH}/dist" --project-name="${CLOUDFLARE_PAGES_PROJECT_NAME}" -popd diff --git a/.circleci/publish_helm_chart.sh b/.circleci/publish_helm_chart.sh deleted file mode 100755 index 718052c72..000000000 --- a/.circleci/publish_helm_chart.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -set -eo pipefail - -GIT_REPO=$( pwd ) -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# shellcheck disable=SC1090,SC1091 -source "${SCRIPT_DIR}/common.sh" - -RELEASE_VERSION="${IMAGE_VERSION_TAG}" -HELM_STABLE_BRANCH="${HELM_STABLE_BRANCH:-"main"}" - -echo "Releasing Helm Chart version ${RELEASE_VERSION}" - -git clone git@github.com:specklesystems/helm.git "${HOME}/helm" - -yq e -i ".version = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml" -yq e -i ".appVersion = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml" -yq e -i ".docker_image_tag = \"${RELEASE_VERSION}\"" "${GIT_REPO}/utils/helm/speckle-server/values.yaml" - -if [[ -n "${CIRCLE_TAG}" || "${CIRCLE_BRANCH}" == "${HELM_STABLE_BRANCH}" ]]; then - # before overwriting the chart with the build version, check if the current chart version - # is not newer than the currently build one - - CURRENT_VERSION="$(grep ^version "${HOME}/helm/charts/speckle-server/Chart.yaml" | grep -o '2\..*')" - echo "${CURRENT_VERSION}" - - .circleci/check_version.py "${CURRENT_VERSION}" "${RELEASE_VERSION}" - if [ $? -eq 1 ] - then - echo "The current helm chart version '${CURRENT_VERSION}' is newer than the version '${RELEASE_VERSION}' we are attempting to publish. Exiting" - exit 1 - fi - rm -rf "${HOME}/helm/charts/speckle-server" - cp -r "${GIT_REPO}/utils/helm/speckle-server" "${HOME}/helm/charts/speckle-server" -else - # overwrite the name of the chart - yq e -i ".name = \"speckle-server-branch-${BRANCH_NAME_TRUNCATED}\"" "${GIT_REPO}/utils/helm/speckle-server/Chart.yaml" - rm -rf "${HOME}/helm/charts/speckle-server-branch-${BRANCH_NAME_TRUNCATED}" - cp -r "${GIT_REPO}/utils/helm/speckle-server" "${HOME}/helm/charts/speckle-server-branch-${BRANCH_NAME_TRUNCATED}" -fi - -cd ~/helm - -git add . -git -c user.email="devops+circleci@speckle.systems" -c user.name="CI" commit -m "CircleCI commit for version '${RELEASE_VERSION}'" -git push diff --git a/.circleci/publish_helm_chart_oci.sh b/.circleci/publish_helm_chart_oci.sh deleted file mode 100755 index dba8953a0..000000000 --- a/.circleci/publish_helm_chart_oci.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -set -eo pipefail - -if [[ -z "${IMAGE_VERSION_TAG}" ]]; then - echo "IMAGE_VERSION_TAG is not set" - exit 1 -fi -if [[ -z "${DOCKER_REG_USER}" ]]; then - echo "DOCKER_REG_USER is not set" - exit 1 -fi -if [[ -z "${DOCKER_REG_PASS}" ]]; then - echo "DOCKER_REG_PASS is not set" - exit 1 -fi - -GIT_REPO=$( pwd ) -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -# shellcheck disable=SC1090,SC1091 -source "${SCRIPT_DIR}/common.sh" - -RELEASE_VERSION="${IMAGE_VERSION_TAG}-chart" -HELM_STABLE_BRANCH="${HELM_STABLE_BRANCH:-"main"}" -DOCKER_HELM_REG_URL="${DOCKER_HELM_REG_URL:-"registry-1.docker.io"}" -DOCKER_HELM_REG_ORG="${DOCKER_HELM_REG_ORG:-"speckle"}" -CHART_NAME="${CHART_NAME:-"speckle-server"}" - -echo "Releasing Helm Chart version ${RELEASE_VERSION} for application version ${IMAGE_VERSION_TAG}" - -yq e -i ".docker_image_tag = \"${IMAGE_VERSION_TAG}\"" "${GIT_REPO}/utils/helm/speckle-server/values.yaml" - -echo "${DOCKER_REG_PASS}" | helm registry login "${DOCKER_HELM_REG_URL}" --username "${DOCKER_REG_USER}" --password-stdin -helm package "${GIT_REPO}/utils/helm/speckle-server" --version "${RELEASE_VERSION}" --app-version "${IMAGE_VERSION_TAG}" --destination "/tmp" -helm push "/tmp/${CHART_NAME}-${RELEASE_VERSION}.tgz" "oci://${DOCKER_HELM_REG_URL}/${DOCKER_HELM_REG_ORG}" diff --git a/.github/codecov.yml b/.github/codecov.yml index 215fa375a..a0c6a2f5f 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -29,7 +29,15 @@ coverage: - shared comment: layout: 'condensed_header, condensed_files, condensed_footer' + after_n_builds: 2 hide_project_coverage: true + behavior: once + require_changes: 'coverage_drop' + require_base: true + require_head: true + +github_checks: + annotations: false flags: server: diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index b13e9996d..32a80ea41 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -16,6 +16,10 @@ on: required: false type: boolean default: false + PUBLISH_LATEST: + required: false + type: boolean + default: false secrets: DATADOG_API_KEY: required: true @@ -38,12 +42,19 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set image tags + id: set-tags + run: | + TAGS="${{ inputs.IMAGE_PREFIX }}/speckle-server:${{ inputs.IMAGE_VERSION_TAG }}" + if [[ "${{ inputs.PUBLISH_LATEST }}" == "true" ]]; then + TAGS="$TAGS,${{ inputs.IMAGE_PREFIX }}/speckle-server:latest" + fi + echo "TAGS=$TAGS" >> "$GITHUB_OUTPUT" - name: Build and push uses: useblacksmith/build-push-action@v1 with: push: true - tags: | - ${{ inputs.IMAGE_PREFIX }}/speckle-server:${{ inputs.IMAGE_VERSION_TAG }} + tags: ${{ steps.set-tags.outputs.TAGS }} # TODO add docker hub tag, but only if we want to push to docker hub file: ./packages/server/Dockerfile build-args: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d115c19a0..83533037e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,7 @@ jobs: DOCKERHUB_USERNAME: 'speckledevops' IMAGE_PREFIX: 'speckle' # without an explicit host, Docker defaults to pushing Docker Hub PUBLISH: true # publish the sourcemaps and include the version in frontend-2 builds + PUBLISH_LATEST: ${{ startsWith(github.ref, 'refs/heads/main') }} secrets: inherit test-deployments: diff --git a/packages/fileimport-service/.env.example b/packages/fileimport-service/.env.example index cf82a1ac6..767e91e0b 100644 --- a/packages/fileimport-service/.env.example +++ b/packages/fileimport-service/.env.example @@ -7,6 +7,7 @@ FF_WORKSPACES_MULTI_REGION_ENABLED=false FF_LEGACY_IFC_IMPORTER_ENABLED=true FF_EXPERIMENTAL_IFC_IMPORTER_ENABLED=false FF_NEXT_GEN_FILE_IMPORTER_ENABLED=false +REDIS_URL="redis://127.0.0.1:6379" # IFC_DOTNET_DLL_PATH='packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll' diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index ce755342f..950afe425 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -1104,7 +1104,7 @@ export type FileUploadMutations = { * recording the provided status, and emitting the needed subscriptions. * Mostly for internal service usage. */ - finishFileImport?: Maybe; + finishFileImport: Scalars['Boolean']['output']; /** * Generate a pre-signed url to which a file can be uploaded. * After uploading the file, call mutation startFileImport to register the completed upload. diff --git a/packages/ifc-import-service/.python-version b/packages/ifc-import-service/.python-version index 0d9339e0f..7eebfafa0 100644 --- a/packages/ifc-import-service/.python-version +++ b/packages/ifc-import-service/.python-version @@ -1 +1 @@ -3.13.4 +3.12.11 diff --git a/packages/ifc-import-service/main.py b/packages/ifc-import-service/main.py index 1392bbfa4..d62005402 100644 --- a/packages/ifc-import-service/main.py +++ b/packages/ifc-import-service/main.py @@ -1,10 +1,39 @@ import asyncio +import logging +import sys + +import structlog +from structlog_to_seq import CelfProcessor from ifc_importer.job_processor import job_processor +def configure_logger() -> structlog.stdlib.BoundLogger: + logging.basicConfig(format="%(message)s", stream=sys.stdout, level=logging.DEBUG) + + structlog.configure( + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + processors=[ + # Prevent exception formatting if logging is not configured + # Add file, line, function information of where log occurred + # Add a timestamp to log message + structlog.processors.TimeStamper(fmt="iso", utc=True), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.stdlib.add_log_level, + CelfProcessor(), + structlog.processors.UnicodeDecoder(), + structlog.processors.JSONRenderer(), + ], + ) + logger = structlog.stdlib.get_logger() + return logger + + async def main(): - task = asyncio.create_task(job_processor()) + logger = configure_logger() + task = asyncio.create_task(job_processor(logger)) # we do not need any sort of signal handling logic, # cause if the context of the job transaction exits, @@ -12,7 +41,9 @@ async def main(): try: await task except Exception as ex: - print(f"Execution failed with exception {ex}") + logger.error( + "Execution failed with exception: {message}", message=str(ex), exc_info=ex + ) raise diff --git a/packages/ifc-import-service/pyproject.toml b/packages/ifc-import-service/pyproject.toml index 1fbeaaf6e..e787505cf 100644 --- a/packages/ifc-import-service/pyproject.toml +++ b/packages/ifc-import-service/pyproject.toml @@ -3,39 +3,46 @@ name = "ifc_importer" version = "0.1.0" description = "Speckle IFC importer worker app" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.12" dependencies = [ - "asyncpg>=0.30.0", - "typed-settings>=24.5.0", - "pydantic>=2.11.7", - "python-dotenv>=1.0.0", - "specklepy>=3.0.1", + "asyncpg>=0.30.0", + "typed-settings>=24.5.0", + "pydantic>=2.11.7", + "python-dotenv>=1.0.0", + "structlog>=25.4.0", + "structlog-to-seq>=21.0.0", + "specklepy[speckleifc]>=3.0.4.dev8", ] [dependency-groups] -dev = ["asyncpg-stubs>=0.30.2", "ruff>=0.12.2"] +dev = [ + "asyncpg-stubs>=0.30.2", + "colorama>=0.4.6", + "colorful>=0.5.7", + "ruff>=0.12.2", +] [tool.ruff] exclude = [".venv", "**/*.yml"] [tool.ruff.lint] select = [ - "A", - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade - "UP", - # flake8-bugbear - "B", - # flake8-simplify - "SIM", - # isort - "I", - # PEP8 naming - "N", - "ASYNC", + "A", + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", + # PEP8 naming + "N", + "ASYNC", ] [build-system] diff --git a/packages/ifc-import-service/src/ifc_importer/job_processor.py b/packages/ifc-import-service/src/ifc_importer/job_processor.py index 0c6757fbf..ebc9ba9c4 100644 --- a/packages/ifc-import-service/src/ifc_importer/job_processor.py +++ b/packages/ifc-import-service/src/ifc_importer/job_processor.py @@ -1,8 +1,19 @@ import asyncio +import tempfile +import time +from pathlib import Path +import structlog +from speckleifc.main import open_and_convert_file from specklepy.core.api.client import ( # pyright: ignore[reportMissingTypeStubs] SpeckleClient, ) +from specklepy.core.api.inputs.file_import_inputs import ( + FileImportErrorInput, + FileImportResult, + FileImportSuccessInput, +) +from specklepy.core.api.models import Version from ifc_importer.domain import FileimportPayload, JobStatus from ifc_importer.repository import get_next_job, set_job_status, setup_connection @@ -25,47 +36,131 @@ def setup_client(job_payload: FileimportPayload) -> SpeckleClient: return speckle_client -async def job_processor(): +async def job_handler( + client: SpeckleClient, job: FileimportPayload, logger: structlog.stdlib.BoundLogger +) -> tuple[Version, float, float]: + # this will create a new temp file and also close it, when the with block closes + with tempfile.NamedTemporaryFile() as file: + start = time.time() + file_path = client.file_import.download_file( + job.project_id, job.blob_id, Path(file.name) + ) + download_end = time.time() + download_duration = download_end - start + logger.info( + "Finished source file download after {download_duration}", + download_duration=download_duration, + ) + version = open_and_convert_file( + file_path=str(file_path), + client=client, + project_id=job.project_id, + model_id=job.model_id, + version_message=f"Created from {job.file_name} upload.", + ) + parse_end = time.time() + parse_duration = parse_end - download_end + logger.info( + "Finished parsing after {parse_duration}", + parse_duration=parse_duration, + ) + return version, download_duration, parse_duration + + +async def job_processor(logger: structlog.stdlib.BoundLogger): + parser = "speckle_ifc" + connection = await setup_connection() while True: - connection = await setup_connection() async with connection.transaction(): job = await get_next_job(connection) if not job: - print("no job found") + logger.info("no job found") await asyncio.sleep(IDLE_TIMEOUT) continue - # speckle_client = setup_client(job.payload) + start = time.time() + speckle_client = setup_client(job.payload) job_id = job.id job_status = JobStatus.QUEUED + ex: Exception | None = None attempt = job.attempt + 1 + version_id: str | None = None + try: - print("starting job") - job_handler = asyncio.sleep(10) + logger = logger.bind(job_id=job_id, project_id=job.payload.project_id) + logger.info("starting job") + handler = job_handler(speckle_client, job.payload, logger) # this will raise a TimeoutError if handler does not complete in time - await asyncio.wait_for( - job_handler, timeout=job.payload.time_out_seconds + version, download_duration, parse_duration = await asyncio.wait_for( + handler, timeout=job.payload.time_out_seconds + ) + version_id = version.id + + duration = time.time() - start + logger.info( + "Finished job after {duration} created version {version_id}", + duration=duration, + version_id=version_id, ) - print("finished job") + _ = speckle_client.file_import.finish_file_import_job( + FileImportSuccessInput( + project_id=job.payload.project_id, + # for some reason, the blob id identifies the job here + job_id=job.payload.blob_id, + result=FileImportResult( + parser=parser, + version_id=version_id, + download_duration_seconds=download_duration, + duration_seconds=duration, + parse_duration_seconds=parse_duration, + ), + ) + ) job_status = JobStatus.SUCCEEDED - except TimeoutError: + except TimeoutError as te: # if it times out we allow re-queueing until it reaches max tries - if job.attempt >= job.max_attempt: + ex = te + if attempt >= job.max_attempt: job_status = JobStatus.FAILED else: job_status = JobStatus.QUEUED # raised if the task is canceled - except Exception: + except Exception as e: # + ex = e job_status = JobStatus.FAILED finally: if job_status == JobStatus.FAILED: # we should be reporting the failure to the server - print("fully failed") + logger.error("job processing failed", exc_info=ex) + try: + _ = speckle_client.file_import.finish_file_import_job( + FileImportErrorInput( + project_id=job.payload.project_id, + # for some reason, the blob id identifies the job here + job_id=job.payload.blob_id, + # job_id=job_id, + reason=str(ex), + result=FileImportResult( + parser=parser, + version_id=None, + download_duration_seconds=0, + duration_seconds=time.time() - start, + parse_duration_seconds=0, + ), + ) + ) + # if the reporting of the failure does not succeed, we're requeueing + # unless we've reached the max attempts + except Exception as ex: + if attempt >= job.max_attempt: + job_status = JobStatus.FAILED + else: + job_status = JobStatus.QUEUED await set_job_status(connection, job_id, job_status, attempt) diff --git a/packages/ifc-import-service/uv.lock b/packages/ifc-import-service/uv.lock index c17ea5eb7..46cf7fb59 100644 --- a/packages/ifc-import-service/uv.lock +++ b/packages/ifc-import-service/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 2 -requires-python = ">=3.13" +requires-python = ">=3.12" [[package]] name = "annotated-types" @@ -18,6 +18,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ @@ -39,6 +40,14 @@ version = "0.30.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162, upload-time = "2024-10-20T00:29:41.88Z" }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025, upload-time = "2024-10-20T00:29:43.352Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243, upload-time = "2024-10-20T00:29:44.922Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059, upload-time = "2024-10-20T00:29:46.891Z" }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596, upload-time = "2024-10-20T00:29:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632, upload-time = "2024-10-20T00:29:50.768Z" }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186, upload-time = "2024-10-20T00:29:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064, upload-time = "2024-10-20T00:29:53.757Z" }, { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" }, { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" }, { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" }, @@ -82,11 +91,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.7.9" +version = "2025.7.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" }, + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, ] [[package]] @@ -95,6 +104,19 @@ version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, @@ -111,6 +133,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorful" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/0c/d180ebf230b771907f46981023a80f62cf592d49673cc5f8a5993aa67bb6/colorful-0.5.7.tar.gz", hash = "sha256:c5452179b56601c178b03d468a5326cc1fe37d9be81d24d0d6bdab36c4b93ad8", size = 209487, upload-time = "2025-06-30T15:24:03.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/98/0d791b3d1eaed89d7d370b5cf9b8079b124da0545559417f394ba21b5532/colorful-0.5.7-py2.py3-none-any.whl", hash = "sha256:495dd3a23151a9568cee8a90fc1174c902ad7ef06655f50b6bddf9e80008da69", size = 201475, upload-time = "2025-06-30T15:24:02.693Z" }, +] + [[package]] name = "deprecated" version = "1.2.18" @@ -210,13 +253,17 @@ dependencies = [ { name = "asyncpg" }, { name = "pydantic" }, { name = "python-dotenv" }, - { name = "specklepy" }, + { name = "specklepy", extra = ["speckleifc"] }, + { name = "structlog" }, + { name = "structlog-to-seq" }, { name = "typed-settings" }, ] [package.dev-dependencies] dev = [ { name = "asyncpg-stubs" }, + { name = "colorama" }, + { name = "colorful" }, { name = "ruff" }, ] @@ -225,22 +272,97 @@ requires-dist = [ { name = "asyncpg", specifier = ">=0.30.0" }, { name = "pydantic", specifier = ">=2.11.7" }, { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "specklepy", specifier = ">=3.0.1" }, + { name = "specklepy", extras = ["speckleifc"], specifier = ">=3.0.4.dev8" }, + { name = "structlog", specifier = ">=25.4.0" }, + { name = "structlog-to-seq", specifier = ">=21.0.0" }, { name = "typed-settings", specifier = ">=24.5.0" }, ] [package.metadata.requires-dev] dev = [ { name = "asyncpg-stubs", specifier = ">=0.30.2" }, + { name = "colorama", specifier = ">=0.4.6" }, + { name = "colorful", specifier = ">=0.5.7" }, { name = "ruff", specifier = ">=0.12.2" }, ] +[[package]] +name = "ifcopenshell" +version = "0.8.3.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "isodate" }, + { name = "lark" }, + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "shapely" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/d9/4e8c4ea647b1e386b6e3fbf1af85e2c5a29f51b5792fa08c2649b7af6103/ifcopenshell-0.8.3.post1-py310-none-macosx_10_15_x86_64.whl", hash = "sha256:d84810f0b32a9201b0f1585770b59fad3d97d4979177117bd66d41bd4db6180c", size = 39850896, upload-time = "2025-07-16T12:27:58.901Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/58589cad183a327a11a24eba4fd88e264db5dcd9f8d66ff41bd46f5c17a9/ifcopenshell-0.8.3.post1-py310-none-macosx_11_0_arm64.whl", hash = "sha256:b9ce95b5bd7bcc6474ff4d56494ef204cf83553af6769a9b4980574e5c219617", size = 39140290, upload-time = "2025-07-16T12:27:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cb/08/f899017d17708cfae3e3421d38e060a348dc4ccd82f8c2f38ad6434e6f90/ifcopenshell-0.8.3.post1-py310-none-manylinux_2_31_x86_64.whl", hash = "sha256:db2206a1542e51cff60afa9c255878867022d24bcab071f3ba3d818af56d0743", size = 40948395, upload-time = "2025-07-16T12:28:05.077Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2e/9a05b9ff3557bc41d487490d18312f42d2e794c91ca4a92311d36009207f/ifcopenshell-0.8.3.post1-py310-none-win_amd64.whl", hash = "sha256:edadd9e0caa56e6d99a473d0416e5ab8561823aa317df49768b6ebfca01d48dc", size = 21618155, upload-time = "2025-07-16T12:27:44.493Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/1f038942a6e395c1da99ab4ba087a0e2e4c3ea12cc328f1952499c3bd16f/ifcopenshell-0.8.3.post1-py311-none-macosx_10_15_x86_64.whl", hash = "sha256:30086e12449acaa99568aad490471a89f7af0a6006173a17cf9d488cd6c62a07", size = 39850896, upload-time = "2025-07-16T12:28:10.736Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/434675f2ce79d643125d985924f50a5c39f892d50f2cf9ebee539a58b781/ifcopenshell-0.8.3.post1-py311-none-macosx_11_0_arm64.whl", hash = "sha256:d693b78f308ff209572d0df8556d1df9d60a19749780b80751e90aa0a5a88266", size = 39140294, upload-time = "2025-07-16T12:27:57.243Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/8293b7c5ae9eb9154a2a2fed57e8c4e1074d2953c72eef14025bdea8f532/ifcopenshell-0.8.3.post1-py311-none-manylinux_2_31_x86_64.whl", hash = "sha256:575ddec101c5787a1c2cb197d190d6831f16c87718fba03f64a7279b3d61abf9", size = 41309793, upload-time = "2025-07-16T12:28:02.061Z" }, + { url = "https://files.pythonhosted.org/packages/23/16/711883ef0ac38d1a778937dc36c2d86cb7e715bd0eaa52ccea6c08a1f79d/ifcopenshell-0.8.3.post1-py311-none-win_amd64.whl", hash = "sha256:fa603eca86d739e8e6d0f84f2c25c4af7987a6a421f632b9344450d346c291e8", size = 21618412, upload-time = "2025-07-16T12:27:44.895Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6d/997b19c109b3407e08a3ca348df03ed6aacc17232579202a06777ecddd15/ifcopenshell-0.8.3.post1-py312-none-macosx_10_15_x86_64.whl", hash = "sha256:af74162aebd120fb12ee8effa843d90cbb2adaeea5870b65adb5b1113e41702d", size = 39861250, upload-time = "2025-07-16T12:27:58.951Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c1/0a7e0ceb1794c90e71bb690860f8f3b94f8b509f6431d2ab572bbab8f698/ifcopenshell-0.8.3.post1-py312-none-macosx_11_0_arm64.whl", hash = "sha256:b97366ccaa1d1af951edb01e6f6e1a2d071af1921f06be98a11b4ff230fa414f", size = 39117663, upload-time = "2025-07-16T12:28:07.061Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d4/31a8e80bcf96b63c83764a0ee8d5cbd6289083ee1c77ec56fd97fdd17ab0/ifcopenshell-0.8.3.post1-py312-none-manylinux_2_31_x86_64.whl", hash = "sha256:b23ff45361512a32490dda0aba5f49525a883e07a5f9c3503ae1ed73a5b7afc3", size = 41590131, upload-time = "2025-07-16T12:28:12.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/03/11ff6fa37490d47de3dc7fa2d0abb107aebf4392943471827d2fbf9344db/ifcopenshell-0.8.3.post1-py312-none-win_amd64.whl", hash = "sha256:b7437ca20b1e567f97919ec5ce229c1888096b761bc866e630da9290f055ae9d", size = 21620495, upload-time = "2025-07-16T12:27:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d6/b69ca24920517a8794ab8d1eaa8b8a4006ad68aed896bd100337fda46a5f/ifcopenshell-0.8.3.post1-py313-none-macosx_10_15_x86_64.whl", hash = "sha256:0add861a10ccdac9bcd0ae8459e0066a072b02858a735c2f6309537ec6e2ec2d", size = 39861245, upload-time = "2025-07-22T12:04:04.645Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c5/b5b2703cb1f43148265ab67bb455273f646e8f5ab207c8167d6174aeeefa/ifcopenshell-0.8.3.post1-py313-none-macosx_11_0_arm64.whl", hash = "sha256:4b9ed9061a49e20cbc0258b098ac2ff10e80f708186cc93b272cd7d5249a2674", size = 39117667, upload-time = "2025-07-22T12:03:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/79/1b/d1f1c9da1ff4c6f4a39eb720d31c1dab82937afe710441cac7a34d61a29d/ifcopenshell-0.8.3.post1-py313-none-manylinux_2_31_x86_64.whl", hash = "sha256:a32e8f13e1c7d34d539e91d293378028224e707bbc7274a045938cc3eb1c66fd", size = 41428607, upload-time = "2025-07-22T12:04:15.746Z" }, + { url = "https://files.pythonhosted.org/packages/24/4f/6f8d39ac56eb1b02748358e1b7a6a1f621a8152a9a6e00995b6b7b5c88f9/ifcopenshell-0.8.3.post1-py313-none-win_amd64.whl", hash = "sha256:90970efba452f5e5203a04120ccc3269ccc150ecf7ca94dffcb854d978ea5612", size = 21620063, upload-time = "2025-07-22T12:04:01.226Z" }, + { url = "https://files.pythonhosted.org/packages/fb/94/ab753c42b80a9db7579a275dbaaa50cdbe0c374827d187d9a6e51b7be3d6/ifcopenshell-0.8.3.post1-py39-none-macosx_10_15_x86_64.whl", hash = "sha256:a7cda970f3d8c9dfb3c7a751760b7773663419d349628e988d556b7646697351", size = 39850895, upload-time = "2025-07-16T12:28:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/7b/68/93d3feb7dc7c6e219ebd6f8b8609320d155b4da497c8dc2c71c1b3bb930b/ifcopenshell-0.8.3.post1-py39-none-macosx_11_0_arm64.whl", hash = "sha256:017ceb7d6399df81b629662951da30cc84b9b7334757f805c0623341a53901a0", size = 39140290, upload-time = "2025-07-16T12:27:58.735Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/f7b5a01c297c897d2a3834b3f2bbbf3fb74b67d8f9a29df6f6f3b1212be8/ifcopenshell-0.8.3.post1-py39-none-manylinux_2_31_x86_64.whl", hash = "sha256:d39d8a67fefd3f457d3a5eeff72e4d052fb88492b1296c70a97c34bcb1f0742b", size = 40917047, upload-time = "2025-07-16T12:28:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a8/3fc2c3d27a0fa052571bc797b27370613cdd9a13c560d01674fa3c71936b/ifcopenshell-0.8.3.post1-py39-none-win_amd64.whl", hash = "sha256:18d79f400cfc1a1ee9d5b59aa252688dd63035f8dbbbea502603e16e91248e7c", size = 21619509, upload-time = "2025-07-16T12:27:47.504Z" }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, +] + +[[package]] +name = "lark" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, +] + [[package]] name = "multidict" version = "6.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, @@ -280,12 +402,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + [[package]] name = "propcache" version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, @@ -345,6 +546,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, @@ -378,6 +593,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -416,27 +643,71 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.2" +version = "0.12.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/cd/01015eb5034605fd98d829c5839ec2c6b4582b479707f7c1c2af861e8258/ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e", size = 5170722, upload-time = "2025-07-24T13:26:37.456Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" }, - { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" }, - { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" }, - { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" }, - { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" }, - { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" }, - { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" }, - { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" }, - { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" }, - { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" }, - { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" }, - { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/de/ad2f68f0798ff15dd8c0bcc2889558970d9a685b3249565a937cd820ad34/ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92", size = 11819133, upload-time = "2025-07-24T13:25:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/c6b65cd0e7fbe60f17e7ad619dca796aa49fbca34bb9bea5f8faf1ec2643/ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a", size = 12501114, upload-time = "2025-07-24T13:25:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/c6bec1dce5ead9f9e6a946ea15e8d698c35f19edc508289d70a577921b30/ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf", size = 11716873, upload-time = "2025-07-24T13:26:01.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/16/cf372d2ebe91e4eb5b82a2275c3acfa879e0566a7ac94d331ea37b765ac8/ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73", size = 11958829, upload-time = "2025-07-24T13:26:03.721Z" }, + { url = "https://files.pythonhosted.org/packages/25/bf/cd07e8f6a3a6ec746c62556b4c4b79eeb9b0328b362bb8431b7b8afd3856/ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac", size = 11626619, upload-time = "2025-07-24T13:26:06.118Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c9/c2ccb3b8cbb5661ffda6925f81a13edbb786e623876141b04919d1128370/ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08", size = 13221894, upload-time = "2025-07-24T13:26:08.292Z" }, + { url = "https://files.pythonhosted.org/packages/6b/58/68a5be2c8e5590ecdad922b2bcd5583af19ba648f7648f95c51c3c1eca81/ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4", size = 14163909, upload-time = "2025-07-24T13:26:10.474Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d1/ef6b19622009ba8386fdb792c0743f709cf917b0b2f1400589cbe4739a33/ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b", size = 13583652, upload-time = "2025-07-24T13:26:13.381Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/1c98c566fe6809a0c83751d825a03727f242cdbe0d142c9e292725585521/ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a", size = 12700451, upload-time = "2025-07-24T13:26:15.488Z" }, + { url = "https://files.pythonhosted.org/packages/24/ff/96058f6506aac0fbc0d0fc0d60b0d0bd746240a0594657a2d94ad28033ba/ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a", size = 12937465, upload-time = "2025-07-24T13:26:17.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/68bc5e7ab96c94b3589d1789f2dd6dd4b27b263310019529ac9be1e8f31b/ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5", size = 11771136, upload-time = "2025-07-24T13:26:20.422Z" }, + { url = "https://files.pythonhosted.org/packages/52/75/7356af30a14584981cabfefcf6106dea98cec9a7af4acb5daaf4b114845f/ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293", size = 11601644, upload-time = "2025-07-24T13:26:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/91c71d27205871737cae11025ee2b098f512104e26ffd8656fd93d0ada0a/ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb", size = 12478068, upload-time = "2025-07-24T13:26:26.134Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/b6b00383cf2f48e8e78e14eb258942fdf2a9bf0287fbf5cdd398b749193a/ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb", size = 12991537, upload-time = "2025-07-24T13:26:28.533Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/053d6445dc7544fb6594785056d8ece61daae7214859ada4a152ad56b6e0/ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9", size = 11751575, upload-time = "2025-07-24T13:26:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/ab16e8259493137598b9149734fec2e06fdeda9837e6f634f5c4e35916da/ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5", size = 12882273, upload-time = "2025-07-24T13:26:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/00/db/c376b0661c24cf770cb8815268190668ec1330eba8374a126ceef8c72d55/ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805", size = 11951564, upload-time = "2025-07-24T13:26:34.994Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422, upload-time = "2025-05-19T11:04:41.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/64/9544dc07dfe80a2d489060791300827c941c451e2910f7364b19607ea352/shapely-2.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2827365b58bf98efb60affc94a8e01c56dd1995a80aabe4b701465d86dcbba43", size = 1833021, upload-time = "2025-05-19T11:04:08.022Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/fb5f545e72e89b6a0f04a0effda144f5be956c9c312c7d4e00dfddbddbcf/shapely-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9c551f7fa7f1e917af2347fe983f21f212863f1d04f08eece01e9c275903fad", size = 1643018, upload-time = "2025-05-19T11:04:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/03/46/61e03edba81de729f09d880ce7ae5c1af873a0814206bbfb4402ab5c3388/shapely-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78dec4d4fbe7b1db8dc36de3031767e7ece5911fb7782bc9e95c5cdec58fb1e9", size = 2986417, upload-time = "2025-05-19T11:04:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/1f/1e/83ec268ab8254a446b4178b45616ab5822d7b9d2b7eb6e27cf0b82f45601/shapely-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:872d3c0a7b8b37da0e23d80496ec5973c4692920b90de9f502b5beb994bbaaef", size = 3098224, upload-time = "2025-05-19T11:04:11.903Z" }, + { url = "https://files.pythonhosted.org/packages/f1/44/0c21e7717c243e067c9ef8fa9126de24239f8345a5bba9280f7bb9935959/shapely-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e2b9125ebfbc28ecf5353511de62f75a8515ae9470521c9a693e4bb9fbe0cf1", size = 3925982, upload-time = "2025-05-19T11:04:13.224Z" }, + { url = "https://files.pythonhosted.org/packages/15/50/d3b4e15fefc103a0eb13d83bad5f65cd6e07a5d8b2ae920e767932a247d1/shapely-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b96cea171b3d7f6786976a0520f178c42792897653ecca0c5422fb1e6946e6d", size = 4089122, upload-time = "2025-05-19T11:04:14.477Z" }, + { url = "https://files.pythonhosted.org/packages/bd/05/9a68f27fc6110baeedeeebc14fd86e73fa38738c5b741302408fb6355577/shapely-2.1.1-cp312-cp312-win32.whl", hash = "sha256:39dca52201e02996df02e447f729da97cfb6ff41a03cb50f5547f19d02905af8", size = 1522437, upload-time = "2025-05-19T11:04:16.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e9/a4560e12b9338842a1f82c9016d2543eaa084fce30a1ca11991143086b57/shapely-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:13d643256f81d55a50013eff6321142781cf777eb6a9e207c2c9e6315ba6044a", size = 1703479, upload-time = "2025-05-19T11:04:18.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/8e/2bc836437f4b84d62efc1faddce0d4e023a5d990bbddd3c78b2004ebc246/shapely-2.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3004a644d9e89e26c20286d5fdc10f41b1744c48ce910bd1867fdff963fe6c48", size = 1832107, upload-time = "2025-05-19T11:04:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/12/a2/12c7cae5b62d5d851c2db836eadd0986f63918a91976495861f7c492f4a9/shapely-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1415146fa12d80a47d13cfad5310b3c8b9c2aa8c14a0c845c9d3d75e77cb54f6", size = 1642355, upload-time = "2025-05-19T11:04:21.035Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/6d28b43d53fea56de69c744e34c2b999ed4042f7a811dc1bceb876071c95/shapely-2.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21fcab88b7520820ec16d09d6bea68652ca13993c84dffc6129dc3607c95594c", size = 2968871, upload-time = "2025-05-19T11:04:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/1017c31e52370b2b79e4d29e07cbb590ab9e5e58cf7e2bdfe363765d6251/shapely-2.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ce6a5cc52c974b291237a96c08c5592e50f066871704fb5b12be2639d9026a", size = 3080830, upload-time = "2025-05-19T11:04:23.997Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fe/f4a03d81abd96a6ce31c49cd8aaba970eaaa98e191bd1e4d43041e57ae5a/shapely-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:04e4c12a45a1d70aeb266618d8cf81a2de9c4df511b63e105b90bfdfb52146de", size = 3908961, upload-time = "2025-05-19T11:04:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/ef/59/7605289a95a6844056a2017ab36d9b0cb9d6a3c3b5317c1f968c193031c9/shapely-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ca74d851ca5264aae16c2b47e96735579686cb69fa93c4078070a0ec845b8d8", size = 4079623, upload-time = "2025-05-19T11:04:27.171Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4d/9fea036eff2ef4059d30247128b2d67aaa5f0b25e9fc27e1d15cc1b84704/shapely-2.1.1-cp313-cp313-win32.whl", hash = "sha256:fd9130501bf42ffb7e0695b9ea17a27ae8ce68d50b56b6941c7f9b3d3453bc52", size = 1521916, upload-time = "2025-05-19T11:04:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/12/d9/6d13b8957a17c95794f0c4dfb65ecd0957e6c7131a56ce18d135c1107a52/shapely-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:ab8d878687b438a2f4c138ed1a80941c6ab0029e0f4c785ecfe114413b498a97", size = 1702746, upload-time = "2025-05-19T11:04:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/60/36/b1452e3e7f35f5f6454d96f3be6e2bb87082720ff6c9437ecc215fa79be0/shapely-2.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c062384316a47f776305ed2fa22182717508ffdeb4a56d0ff4087a77b2a0f6d", size = 1833482, upload-time = "2025-05-19T11:04:30.852Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ca/8e6f59be0718893eb3e478141285796a923636dc8f086f83e5b0ec0036d0/shapely-2.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4ecf6c196b896e8f1360cc219ed4eee1c1e5f5883e505d449f263bd053fb8c05", size = 1642256, upload-time = "2025-05-19T11:04:32.068Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/0053aea449bb1d4503999525fec6232f049abcdc8df60d290416110de943/shapely-2.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb00070b4c4860f6743c600285109c273cca5241e970ad56bb87bef0be1ea3a0", size = 3016614, upload-time = "2025-05-19T11:04:33.7Z" }, + { url = "https://files.pythonhosted.org/packages/ee/53/36f1b1de1dfafd1b457dcbafa785b298ce1b8a3e7026b79619e708a245d5/shapely-2.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d14a9afa5fa980fbe7bf63706fdfb8ff588f638f145a1d9dbc18374b5b7de913", size = 3093542, upload-time = "2025-05-19T11:04:34.952Z" }, + { url = "https://files.pythonhosted.org/packages/b9/bf/0619f37ceec6b924d84427c88835b61f27f43560239936ff88915c37da19/shapely-2.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b640e390dabde790e3fb947198b466e63223e0a9ccd787da5f07bcb14756c28d", size = 3945961, upload-time = "2025-05-19T11:04:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/93/c9/20ca4afeb572763b07a7997f00854cb9499df6af85929e93012b189d8917/shapely-2.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:69e08bf9697c1b73ec6aa70437db922bafcea7baca131c90c26d59491a9760f9", size = 4089514, upload-time = "2025-05-19T11:04:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/27036a5a560b80012a544366bceafd491e8abb94a8db14047b5346b5a749/shapely-2.1.1-cp313-cp313t-win32.whl", hash = "sha256:ef2d09d5a964cc90c2c18b03566cf918a61c248596998a0301d5b632beadb9db", size = 1540607, upload-time = "2025-05-19T11:04:38.925Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061, upload-time = "2025-05-19T11:04:40.082Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] @@ -450,7 +721,7 @@ wheels = [ [[package]] name = "specklepy" -version = "3.0.1" +version = "3.0.4.dev8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appdirs" }, @@ -462,18 +733,41 @@ dependencies = [ { name = "pydantic-settings" }, { name = "ujson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/e1/92a88f3651271177200e1c222720c125d317eb95766dd3a68548f50d4ba5/specklepy-3.0.1.tar.gz", hash = "sha256:a535e73cb378cf7dee35c60a45b48b2c4866d5216b9abcf0426d8223091d3a2e", size = 201758, upload-time = "2025-06-06T14:16:08.84Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/c5/f4cf475294ce582bfd1ca1409ca40e1005a34b5c574ffa41bc47ff512d89/specklepy-3.0.4.dev8.tar.gz", hash = "sha256:757ae3bb90c00610a4d3f3a38190a999d261ba9b4f9b7cb7254c24f65bebe54b", size = 92785, upload-time = "2025-07-28T07:45:49.645Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/c4/fa934c65d6cb9351da126a2a2e738c57362da415b7acc10fe43f2aa17efa/specklepy-3.0.1-py3-none-any.whl", hash = "sha256:91cd6a32c462db9969cc4872f67b97ae0c861bdb75b78aba9597ef67936aa675", size = 110323, upload-time = "2025-06-06T14:16:07.538Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4d/c792d2bd9fc0ea4df66132a2c8343932fcc814cb4981cc07b60fc7625bfe/specklepy-3.0.4.dev8-py3-none-any.whl", hash = "sha256:c528df2b34a01a9223cb01a047453f49a102a4bb7259a649ad2e599ec1626e88", size = 140223, upload-time = "2025-07-28T07:45:45.637Z" }, +] + +[package.optional-dependencies] +speckleifc = [ + { name = "ifcopenshell" }, +] + +[[package]] +name = "structlog" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b9/6e672db4fec07349e7a8a8172c1a6ae235c58679ca29c3f86a61b5e59ff3/structlog-25.4.0.tar.gz", hash = "sha256:186cd1b0a8ae762e29417095664adf1d6a31702160a46dacb7796ea82f7409e4", size = 1369138, upload-time = "2025-06-02T08:21:12.971Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/4a/97ee6973e3a73c74c8120d59829c3861ea52210667ec3e7a16045c62b64d/structlog-25.4.0-py3-none-any.whl", hash = "sha256:fe809ff5c27e557d14e613f45ca441aabda051d119ee5a0102aaba6ce40eed2c", size = 68720, upload-time = "2025-06-02T08:21:11.43Z" }, +] + +[[package]] +name = "structlog-to-seq" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/ec/18318d42798cc8b68a17e7aa39817de68d2c87c6f903405014ce03be06ad/structlog_to_seq-21.0.0.tar.gz", hash = "sha256:9ea0438625a6212d6804747136e997808d172369a1f84fc5384e332f7c695568", size = 4743, upload-time = "2021-03-12T11:19:34.319Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/5f/1bda50d362fc6a80aa1a810c5ad6bf6500cc28c8d7239b3e57b058ac509c/structlog_to_seq-21.0.0-py3-none-any.whl", hash = "sha256:6693a315d5f87cd14713929a5d04c97961e9fbb416dd7ebcd6da734dbcdacef1", size = 5338, upload-time = "2021-03-12T11:19:33.111Z" }, ] [[package]] name = "typed-settings" -version = "24.6.0" +version = "25.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/91/f39cff1a6eb43ba5d8e7eb810405b0a7abed7b7033f7725a09796c625547/typed_settings-24.6.0.tar.gz", hash = "sha256:9a5595de33f80452a2038e018bc45508fffc238a8752f08f5c03dc8e7bc0d1e2", size = 29469194, upload-time = "2024-11-07T09:46:28.562Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/c2/292035683029b7a6c00d952baf1f0ac828e03dd912c383760762eb4a937b/typed_settings-25.0.0.tar.gz", hash = "sha256:29bafd31cd4f5e00fe380c3f003a771d70beae700970512a8e55f142aff5c113", size = 3478917, upload-time = "2025-07-25T20:37:25.415Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/65/659c1bebd863edc9184715fd1cf2475926cead71d83535d980577bfa2439/typed_settings-24.6.0-py3-none-any.whl", hash = "sha256:22221598a821395700beaafd8eab0e1bc596b394767aa37e0926c5f74d0b6a62", size = 58394, upload-time = "2024-11-07T09:46:25.079Z" }, + { url = "https://files.pythonhosted.org/packages/39/20/0fcbf638c7e47b6270dae533e32b7a3d71a733dd33f4aaae1af3747ac3b7/typed_settings-25.0.0-py3-none-any.whl", hash = "sha256:d95ec61a7f8109f91b6bb910237cd801e5d861face2bf09e6563a6a76637fc63", size = 60650, upload-time = "2025-07-25T20:37:23.807Z" }, ] [[package]] @@ -503,6 +797,16 @@ version = "5.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885, upload-time = "2024-05-14T02:02:34.233Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642, upload-time = "2024-05-14T02:01:04.055Z" }, + { url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807, upload-time = "2024-05-14T02:01:05.25Z" }, + { url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972, upload-time = "2024-05-14T02:01:06.458Z" }, + { url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686, upload-time = "2024-05-14T02:01:07.618Z" }, + { url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591, upload-time = "2024-05-14T02:01:08.901Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853, upload-time = "2024-05-14T02:01:10.772Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689, upload-time = "2024-05-14T02:01:12.214Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576, upload-time = "2024-05-14T02:01:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764, upload-time = "2024-05-14T02:01:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211, upload-time = "2024-05-14T02:01:17.567Z" }, { url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646, upload-time = "2024-05-14T02:01:19.26Z" }, { url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806, upload-time = "2024-05-14T02:01:20.593Z" }, { url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975, upload-time = "2024-05-14T02:01:21.904Z" }, @@ -539,6 +843,17 @@ version = "1.17.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, @@ -575,6 +890,23 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, diff --git a/packages/objectloader2/package.json b/packages/objectloader2/package.json index 2e976b7a1..226a835b0 100644 --- a/packages/objectloader2/package.json +++ b/packages/objectloader2/package.json @@ -33,8 +33,7 @@ "author": "AEC Systems", "license": "Apache-2.0", "dependencies": { - "@speckle/shared": "workspace:^", - "dexie": "^4.0.11" + "@speckle/shared": "workspace:^" }, "devDependencies": { "@types/lodash": "^4.17.5", diff --git a/packages/objectloader2/src/core/objectLoader2Factory.ts b/packages/objectloader2/src/core/objectLoader2Factory.ts index 641055d36..fc5f950d3 100644 --- a/packages/objectloader2/src/core/objectLoader2Factory.ts +++ b/packages/objectloader2/src/core/objectLoader2Factory.ts @@ -48,7 +48,6 @@ export class ObjectLoader2Factory { } if (getFeatureFlag(ObjectLoader2Flags.USE_CACHE) === 'true') { database = new IndexedDatabase({ - logger: log, indexedDB: params.options?.indexedDB, keyRange: params.options?.keyRange }) diff --git a/packages/objectloader2/src/core/stages/ItemStore.ts b/packages/objectloader2/src/core/stages/ItemStore.ts new file mode 100644 index 000000000..de57d193e --- /dev/null +++ b/packages/objectloader2/src/core/stages/ItemStore.ts @@ -0,0 +1,196 @@ +/* eslint-disable @typescript-eslint/no-base-to-string */ +/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-function-type */ +import { isSafari } from '@speckle/shared' +import { Item } from '../../types/types.js' + +/** + * A wrapper class for IndexedDB to simplify common database operations. + */ +export interface ItemStoreOptions { + indexedDB?: IDBFactory + keyRange?: { + bound: Function + lowerBound: Function + upperBound: Function + } +} +export class ItemStore { + #options: ItemStoreOptions + + #db: IDBDatabase | undefined = undefined + readonly #dbName: string + readonly #storeName: string + + constructor(options: ItemStoreOptions, dbName: string, storeName: string) { + this.#options = options + this.#dbName = dbName + this.#storeName = storeName + } + + /** + * Initializes the database connection and creates the object store if needed. + * This must be called before any other database operations. + */ + async init(): Promise { + if (this.#db) return + await this.#safariFix() + return this.#openDatabase() + } + + /** + * Opens the database, and if there's an error, deletes the database and tries again. + */ + async #openDatabase(): Promise { + const idb = this.#options.indexedDB ?? indexedDB + + return new Promise((resolve, reject) => { + const request = idb.open(this.#dbName, 1) + + request.onerror = (): any => { + console.warn( + `Failed to open database: ${this.#dbName}, deleting and trying again` + ) + // Delete the database and try again + const deleteRequest = idb.deleteDatabase(this.#dbName) + deleteRequest.onsuccess = (): any => { + // Try opening again after deletion + void this.#openDatabase().then(resolve).catch(reject) + } + deleteRequest.onerror = (): any => { + reject(`Failed to delete and reopen database: ${this.#dbName}`) + } + } + + request.onupgradeneeded = (event): any => { + const db = (event.target as IDBOpenDBRequest).result + if (db.objectStoreNames.contains(this.#storeName)) { + db.deleteObjectStore(this.#storeName) + } + db.createObjectStore(this.#storeName, { keyPath: 'baseId' }) + } + + request.onsuccess = (event): any => { + this.#db = (event.target as IDBOpenDBRequest).result + resolve() + } + }) + } + + #getDB(): IDBDatabase { + if (!this.#db) { + throw new Error('Database not initialized. Call init() first.') + } + return this.#db + } + + /** + * Fixes a Safari bug where IndexedDB requests get lost and never resolve - invoke before you use IndexedDB + * @link Credits and more info: https://github.com/jakearchibald/safari-14-idb-fix + */ + async #safariFix(): Promise { + // No point putting other browsers or older versions of Safari through this mess. + if (!isSafari() || !this.#options.indexedDB?.databases) return Promise.resolve() + + let intervalId: ReturnType + + return new Promise((resolve: () => void) => { + const tryIdb = (): Promise | undefined => + this.#options.indexedDB?.databases().finally(resolve) + intervalId = setInterval(() => { + void tryIdb() + }, 100) + void tryIdb() + }).finally(() => clearInterval(intervalId)) + } + + /** + * Inserts or updates an array of items in a single transaction. + * @param data The array of items to insert. + */ + bulkInsert(data: Item[]): Promise { + return new Promise((resolve, reject) => { + try { + const transaction = this.#getDB().transaction(this.#storeName, 'readwrite', { + durability: 'relaxed' + }) + const store = transaction.objectStore(this.#storeName) + + transaction.onerror = (): any => { + reject(`Transaction error: ${transaction.error}`) + } + transaction.oncomplete = (): any => { + resolve() + } + + data.forEach((item) => store.put(item)) + } catch (error) { + reject(error) + } + }) + } + + /** + * Retrieves an array of items from the object store based on their IDs. + * @param ids The array of IDs to retrieve. + */ + bulkGet(ids: string[]): Promise<(Item | undefined)[]> { + return new Promise((resolve, reject) => { + if (ids.length === 0) { + return resolve([]) + } + try { + const transaction = this.#getDB().transaction(this.#storeName, 'readonly', { + durability: 'relaxed' + }) + const store = transaction.objectStore(this.#storeName) + const promises: Promise[] = [] + + for (const id of ids) { + promises.push( + new Promise((resolveGet, rejectGet) => { + const request = store.get(id) + request.onerror = (): void => + rejectGet(`Request error for id ${id}: ${request.error}`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + request.onsuccess = (): void => resolveGet(request.result) + }) + ) + } + + Promise.all(promises) + .then((results) => { + resolve(results) + }) + .catch(reject) + } catch (error) { + reject(error) + } + }) + } + + /** + * Retrieves all items from the object store. + */ + getAll(): Promise { + return new Promise((resolve, reject) => { + try { + const transaction = this.#getDB().transaction(this.#storeName, 'readonly') + const store = transaction.objectStore(this.#storeName) + const request = store.getAll() + + request.onerror = (): any => reject(`Request error: ${request.error}`) + request.onsuccess = (): any => resolve(request.result) + } catch (error) { + reject(error) + } + }) + } + + close(): void { + if (!this.#db) return + this.#db.close() + this.#db = undefined + } +} diff --git a/packages/objectloader2/src/core/stages/__snapshots__/serverDownloader.spec.ts.snap b/packages/objectloader2/src/core/stages/__snapshots__/serverDownloader.spec.ts.snap index be81cb77d..067d349f6 100644 --- a/packages/objectloader2/src/core/stages/__snapshots__/serverDownloader.spec.ts.snap +++ b/packages/objectloader2/src/core/stages/__snapshots__/serverDownloader.spec.ts.snap @@ -56,6 +56,31 @@ exports[`downloader > download batch of three 1`] = ` ] `; +exports[`downloader > download batch of three with Objects.Other.RawEncoding 1`] = ` +[ + { + "base": { + "id": "id1", + "speckle_type": "type", + }, + "baseId": "id1", + "size": 34, + }, + { + "base": undefined, + "baseId": "id2", + }, + { + "base": { + "id": "id3", + "speckle_type": "type", + }, + "baseId": "id3", + "size": 34, + }, +] +`; + exports[`downloader > download batch of two 1`] = ` [ { diff --git a/packages/objectloader2/src/core/stages/cacheReader.spec.ts b/packages/objectloader2/src/core/stages/cacheReader.spec.ts index 66d5cebff..a6e04fee0 100644 --- a/packages/objectloader2/src/core/stages/cacheReader.spec.ts +++ b/packages/objectloader2/src/core/stages/cacheReader.spec.ts @@ -16,6 +16,7 @@ describe('CacheReader testing', () => { items: new Map([[i1.baseId, i1.base!]]) }), deferments, + () => {}, { maxCacheReadSize: 1, maxCacheWriteSize: 1, diff --git a/packages/objectloader2/src/core/stages/cacheReader.ts b/packages/objectloader2/src/core/stages/cacheReader.ts index 448193ef9..3d526be2d 100644 --- a/packages/objectloader2/src/core/stages/cacheReader.ts +++ b/packages/objectloader2/src/core/stages/cacheReader.ts @@ -75,7 +75,11 @@ export class CacheReader { this.#notFoundQueue?.add(batch[i]) } } - this.#logger('readBatch: left, time', items.length, performance.now() - start) + this.#logger( + `readBatch: batch ${batch.length}, time ${ + performance.now() - start + } ms left ${this.#readQueue?.count()}` + ) } disposeAsync(): Promise { diff --git a/packages/objectloader2/src/core/stages/cacheWriter.spec.ts b/packages/objectloader2/src/core/stages/cacheWriter.spec.ts new file mode 100644 index 000000000..cc82f330a --- /dev/null +++ b/packages/objectloader2/src/core/stages/cacheWriter.spec.ts @@ -0,0 +1,213 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { Item } from '../../types/types.js' +import { CacheWriter } from './cacheWriter.js' +import { DefermentManager } from '../../deferment/defermentManager.js' +import { Database } from '../interfaces.js' +import { MemoryCache, MemoryCacheOptions } from '../../deferment/MemoryCache.js' +import { CacheOptions } from '../options.js' +import { CustomLogger } from '../../types/functions.js' + +// Mock implementations +class MockDatabase implements Database { + savedItems: Item[] = [] + + getAll(/* keys */): Promise<(Item | undefined)[]> { + return Promise.resolve([]) + } + + saveBatch({ batch }: { batch: Item[] }): Promise { + this.savedItems.push(...batch) + return Promise.resolve() + } + + disposeAsync(): Promise { + return Promise.resolve() + } +} + +describe('CacheWriter', () => { + let database: MockDatabase + let defermentManager: DefermentManager + let cacheWriter: CacheWriter + let logger: CustomLogger + let requestItemMock: (id: string) => void + let options: CacheOptions + let memoryCache: MemoryCache + + beforeEach(() => { + database = new MockDatabase() + logger = vi.fn() as CustomLogger + const memoryCacheOptions: MemoryCacheOptions = { + maxSizeInMb: 100, + ttlms: 60000 + } + memoryCache = new MemoryCache(memoryCacheOptions, logger) + defermentManager = new DefermentManager(memoryCache, logger) + requestItemMock = vi.fn() + + options = { + maxCacheWriteSize: 10, + maxCacheBatchWriteWait: 50, + maxCacheReadSize: 10, + maxCacheBatchReadWait: 50, + maxWriteQueueSize: 100 + } + + cacheWriter = new CacheWriter( + database, + logger, + defermentManager, + options, + requestItemMock + ) + }) + + afterEach(async () => { + await cacheWriter.disposeAsync() + }) + + it('should write items to the database', async () => { + const item: Item = { + baseId: 'test-id', + base: { id: 'test-id', speckle_type: 'test-type' } + } + + cacheWriter.add(item) + + // Wait for batch queue to process + await new Promise((resolve) => + setTimeout(resolve, options.maxCacheBatchWriteWait + 10) + ) + + expect(database.savedItems).toHaveLength(1) + expect(database.savedItems[0].baseId).toBe('test-id') + expect(database.savedItems[0].base).toEqual({ + id: 'test-id', + speckle_type: 'test-type' + }) + }) + + it('should write multiple items in a batch', async () => { + const items = [ + { + baseId: 'item1', + base: { id: 'item1', speckle_type: 'test-type' } + }, + { + baseId: 'item2', + base: { id: 'item2', speckle_type: 'test-type' } + }, + { + baseId: 'item3', + base: { id: 'item3', speckle_type: 'test-type' } + } + ] + + items.forEach((item) => cacheWriter.add(item)) + + // Wait for batch queue to process + await new Promise((resolve) => + setTimeout(resolve, options.maxCacheBatchWriteWait + 10) + ) + + expect(database.savedItems).toHaveLength(3) + expect(database.savedItems.map((item) => item.baseId)).toEqual([ + 'item1', + 'item2', + 'item3' + ]) + }) + + it('should call undefer on the defermentManager', () => { + const spy = vi.spyOn(defermentManager, 'undefer') + const item: Item = { + baseId: 'test-id', + base: { id: 'test-id', speckle_type: 'test-type' } + } + + cacheWriter.add(item) + + expect(spy).toHaveBeenCalledWith(item, requestItemMock) + }) + + it('should handle items with id but no base', async () => { + const item: Item = { + baseId: 'test-id-no-base' + // No base property + } + + // Adding an item with no base should not throw an error + expect(() => cacheWriter.add(item)).not.toThrow() + + // Wait for batch queue to process + await new Promise((resolve) => + setTimeout(resolve, options.maxCacheBatchWriteWait + 10) + ) + + expect(database.savedItems).toHaveLength(1) + expect(database.savedItems[0].baseId).toBe('test-id-no-base') + expect(database.savedItems[0].base).toBeUndefined() + }) + + it('should process items in batches according to maxCacheWriteSize', async () => { + const spy = vi.spyOn(database, 'saveBatch') + const smallBatchOptions: CacheOptions = { + ...options, + maxCacheWriteSize: 2, // Set small batch size + maxCacheBatchWriteWait: 100 + } + + const smallBatchCacheWriter = new CacheWriter( + database, + logger, + defermentManager, + smallBatchOptions, + requestItemMock + ) + + // Add 5 items + const items = Array.from({ length: 5 }, (_, i) => ({ + baseId: `batch-item-${i}`, + base: { id: `batch-item-${i}`, speckle_type: 'test-type' } + })) + + items.forEach((item) => smallBatchCacheWriter.add(item)) + + // Wait for batch queue to process + await new Promise((resolve) => + setTimeout(resolve, smallBatchOptions.maxCacheBatchWriteWait * 2 + 50) + ) + + // With batch size 2, we expect 3 calls to saveBatch: 2 items + 2 items + 1 item + expect(spy).toHaveBeenCalledTimes(3) + + await smallBatchCacheWriter.disposeAsync() + }) + + it('should be disposed correctly', async () => { + expect(cacheWriter.isDisposed).toBe(false) + await cacheWriter.disposeAsync() + expect(cacheWriter.isDisposed).toBe(true) + }) + + it('should write directly with writeAll method', async () => { + const items = [ + { + baseId: 'direct1', + base: { id: 'direct1', speckle_type: 'test-type' } + }, + { + baseId: 'direct2', + base: { id: 'direct2', speckle_type: 'test-type' } + } + ] + + await cacheWriter.writeAll(items) + + expect(database.savedItems).toHaveLength(2) + expect(database.savedItems.map((item) => item.baseId)).toEqual([ + 'direct1', + 'direct2' + ]) + }) +}) diff --git a/packages/objectloader2/src/core/stages/cacheWriter.ts b/packages/objectloader2/src/core/stages/cacheWriter.ts index d8683429b..8909d5697 100644 --- a/packages/objectloader2/src/core/stages/cacheWriter.ts +++ b/packages/objectloader2/src/core/stages/cacheWriter.ts @@ -46,7 +46,11 @@ export class CacheWriter implements Queue { async writeAll(items: Item[]): Promise { const start = performance.now() await this.#database.saveBatch({ batch: items }) - this.#logger('writeBatch: left, time', items.length, performance.now() - start) + this.#logger( + `writeBatch: wrote ${items.length}, time ${ + performance.now() - start + } ms left ${this.#writeQueue?.count()}` + ) } async disposeAsync(): Promise { diff --git a/packages/objectloader2/src/core/stages/indexedDatabase.ts b/packages/objectloader2/src/core/stages/indexedDatabase.ts index c10e60447..7dfb39d37 100644 --- a/packages/objectloader2/src/core/stages/indexedDatabase.ts +++ b/packages/objectloader2/src/core/stages/indexedDatabase.ts @@ -1,26 +1,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { CustomLogger } from '../../types/functions.js' import { Item } from '../../types/types.js' -import { isSafari } from '@speckle/shared' -import { Dexie, DexieOptions, Table } from 'dexie' import { Database } from '../interfaces.js' import BatchingQueue from '../../queues/batchingQueue.js' - -export class ObjectStore extends Dexie { - static #databaseName: string = 'speckle-cache' - objects!: Table // Table type: - - constructor(options: DexieOptions) { - super(ObjectStore.#databaseName, options) - - this.version(1).stores({ - objects: 'baseId, item' // baseId is primary key - }) - } -} +import { ItemStore } from './ItemStore.js' export interface IndexedDatabaseOptions { - logger?: CustomLogger indexedDB?: IDBFactory keyRange?: { bound: Function @@ -31,97 +15,35 @@ export interface IndexedDatabaseOptions { export default class IndexedDatabase implements Database { #options: IndexedDatabaseOptions - #logger: CustomLogger - - #cacheDB?: ObjectStore - + #cacheDB: ItemStore #writeQueue: BatchingQueue | undefined - // #count: number = 0 - constructor(options: IndexedDatabaseOptions) { this.#options = options - this.#logger = options.logger || ((): void => {}) + this.#cacheDB = new ItemStore( + { + indexedDB: this.#options.indexedDB, + keyRange: this.#options.keyRange + }, + 'speckle-cache', + 'cache' + ) } async getAll(keys: string[]): Promise<(Item | undefined)[]> { - await this.#setupCacheDb() - let items: (Item | undefined)[] = [] - // this.#count++ - // const startTime = performance.now() - // this.#logger('Start read ' + x + ' ' + batch.length) - - //faster than BulkGet with dexie - await this.#cacheDB!.transaction('r', this.#cacheDB!.objects, async () => { - const gets = keys.map((key) => this.#cacheDB!.objects.get(key)) - const cachedData = await Promise.all(gets) - items = cachedData - }) - // const endTime = performance.now() - // const duration = endTime - startTime - //this.#logger('Saved batch ' + x + ' ' + batch.length + ' ' + duration / TIME_MS.second) - - return items - } - - async #openDatabase(): Promise { - const db = new ObjectStore({ - indexedDB: this.#options.indexedDB ?? globalThis.indexedDB, - IDBKeyRange: this.#options.keyRange ?? IDBKeyRange, - chromeTransactionDurability: 'relaxed' - }) - await db.open() - return db - } - - async #setupCacheDb(): Promise { - if (this.#cacheDB !== undefined) { - return - } - - // Initialize - await this.#safariFix() - this.#cacheDB = await this.#openDatabase() + await this.#cacheDB.init() + return await this.#cacheDB.bulkGet(keys) } async saveBatch(params: { batch: Item[] }): Promise { - await this.#setupCacheDb() + await this.#cacheDB.init() const { batch } = params - //const x = this.#count - //this.#count++ - - // const startTime = performance.now() - // this.#logger('Start save ' + x + ' ' + batch.length) - await this.#cacheDB!.objects.bulkPut(batch) - // const endTime = performance.now() - // const duration = endTime - startTime - //this.#logger('Saved batch ' + x + ' ' + batch.length + ' ' + duration / TIME_MS.second) - } - - /** - * Fixes a Safari bug where IndexedDB requests get lost and never resolve - invoke before you use IndexedDB - * @link Credits and more info: https://github.com/jakearchibald/safari-14-idb-fix - */ - async #safariFix(): Promise { - // No point putting other browsers or older versions of Safari through this mess. - if (!isSafari() || !this.#options.indexedDB?.databases) return Promise.resolve() - - let intervalId: ReturnType - - return new Promise((resolve: () => void) => { - const tryIdb = (): Promise | undefined => - this.#options.indexedDB?.databases().finally(resolve) - intervalId = setInterval(() => { - void tryIdb() - }, 100) - void tryIdb() - }).finally(() => clearInterval(intervalId)) + await this.#cacheDB.bulkInsert(batch) } async disposeAsync(): Promise { - this.#cacheDB?.close() - this.#cacheDB = undefined await this.#writeQueue?.disposeAsync() this.#writeQueue = undefined + this.#cacheDB.close() } } diff --git a/packages/objectloader2/src/core/stages/serverDownloader.spec.ts b/packages/objectloader2/src/core/stages/serverDownloader.spec.ts index b1236b542..885e5e16e 100644 --- a/packages/objectloader2/src/core/stages/serverDownloader.spec.ts +++ b/packages/objectloader2/src/core/stages/serverDownloader.spec.ts @@ -118,6 +118,77 @@ describe('downloader', () => { await downloader.disposeAsync() }) + test('download batch of three with Objects.Other.RawEncoding', async () => { + const fetchMocker = createFetchMock(vi) + const i1: Item = { baseId: 'id1', base: { id: 'id1', speckle_type: 'type' } } + const i2: Item = { + baseId: 'id2', + base: { id: 'id2', speckle_type: 'Objects.Other.RawEncoding' } + } + const i3: Item = { baseId: 'id3', base: { id: 'id3', speckle_type: 'type' } } + fetchMocker.mockResponseOnce( + 'id1\t' + + JSON.stringify(i1.base) + + '\nid2\t' + + JSON.stringify(i2.base) + + '\nid3\t' + + JSON.stringify(i3.base) + + '\n' + ) + + const gathered = new AsyncGeneratorQueue() + const downloader = new ServerDownloader({ + serverUrl: 'http://speckle.test', + streamId: 'streamId', + objectId: 'objectId', + token: 'token', + + fetch: fetchMocker + }) + downloader.initializePool({ + results: gathered, + total: 3, + maxDownloadBatchWait: 200 + }) + downloader.add('id1') + downloader.add('id2') + downloader.add('id3') + await downloader.disposeAsync() + const r = [] + for await (const x of gathered.consume()) { + r.push(x) + if (r.length >= 3) { + break + } + } + + expect(r).toMatchSnapshot() + await downloader.disposeAsync() + }) + + test("download Objects.Other.RawEncoding doesn't exist", async () => { + const fetchMocker = createFetchMock(vi) + const i: Item = { + baseId: 'id', + base: { + id: 'id', + speckle_type: 'Objects.Other.RawEncoding', + __closure: { childIds: 1 } + } + } + fetchMocker.mockResponseOnce(JSON.stringify(i.base)) + const downloader = new ServerDownloader({ + serverUrl: 'http://speckle.test', + streamId: 'streamId', + objectId: i.baseId, + token: 'token', + + fetch: fetchMocker + }) + await expect(downloader.downloadSingle()).rejects.toThrow() + await downloader.disposeAsync() + }) + test('download single exists', async () => { const fetchMocker = createFetchMock(vi) const i: Item = { diff --git a/packages/objectloader2/src/core/stages/serverDownloader.ts b/packages/objectloader2/src/core/stages/serverDownloader.ts index 77979d770..b338d392f 100644 --- a/packages/objectloader2/src/core/stages/serverDownloader.ts +++ b/packages/objectloader2/src/core/stages/serverDownloader.ts @@ -1,7 +1,7 @@ import BatchedPool from '../../queues/batchedPool.js' import Queue from '../../queues/queue.js' import { ObjectLoaderRuntimeError } from '../../types/errors.js' -import { Fetcher, isBase, take } from '../../types/functions.js' +import { Fetcher, indexOf, isBase, take } from '../../types/functions.js' import { Item } from '../../types/types.js' import { Downloader } from '../interfaces.js' @@ -29,6 +29,9 @@ export default class ServerDownloader implements Downloader { #decoder = new TextDecoder('utf-8', { fatal: true }) #decodedBytesCount = 0 + #rawString: string = 'Objects.Other.RawEncoding' + #rawEncoding: Uint8Array + constructor(options: ServerDownloaderOptions) { this.#options = options this.#fetch = @@ -51,6 +54,9 @@ export default class ServerDownloader implements Downloader { this.#requestUrlRootObj = `${this.#options.serverUrl}/objects/${ this.#options.streamId }/${this.#options.objectId}/single` + + const encoder = new TextEncoder() + this.#rawEncoding = encoder.encode(this.#rawString) } #getDownloadCountAndSizes(total: number): number[] { @@ -140,7 +146,7 @@ Chrome's behavior: Chrome generally handles larger data sizes without this speci const { done, value } = await reader.read() if (done) break - leftover = await this.processArray(leftover, value, keys, async () => { + leftover = await this.#processArray(leftover, value, keys, async () => { count++ if (count % 1000 === 0) { await new Promise((resolve) => setTimeout(resolve, 100)) //allow other stuff to happen @@ -158,14 +164,14 @@ Chrome's behavior: Chrome generally handles larger data sizes without this speci } } - async processArray( + async #processArray( leftover: Uint8Array, value: Uint8Array, keys: Set, callback: () => Promise ): Promise { //this concat will allocate a new array - const combined = this.concatUint8Arrays(leftover, value) + const combined = this.#concatUint8Arrays(leftover, value) let start = 0 //subarray doesn't allocate @@ -173,25 +179,29 @@ Chrome's behavior: Chrome generally handles larger data sizes without this speci if (combined[i] === 0x0a) { const line = combined.subarray(start, i) // line without \n //strings are allocated here - const item = this.processLine(line) - this.#results?.add(item) + const item = this.#processLine(line) start = i + 1 await callback() keys.delete(item.baseId) + this.#results?.add(item) } } return combined.subarray(start) // carry over remainder } - processLine(line: Uint8Array): Item { + #processLine(line: Uint8Array): Item { for (let i = 0; i < line.length; i++) { if (line[i] === 0x09) { //this is a tab const baseId = this.decodeChunk(line.subarray(0, i)) - const json = line.subarray(i + 1) - const base = this.decodeChunk(json) + const jsonBytes = line.subarray(i + 1) + + if (!this.#isValidBytes(jsonBytes)) { + return { baseId, base: undefined } + } + const base = this.decodeChunk(jsonBytes) const item = this.#processJson(baseId, base) - item.size = json.length + item.size = jsonBytes.length return item } } @@ -213,8 +223,21 @@ Chrome's behavior: Chrome generally handles larger data sizes without this speci throw new ObjectLoaderRuntimeError(`${baseId} is not a base`) } } + #isValidString(json: string): boolean { + if (!json.includes(this.#rawString)) { + return true + } + return false + } - concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { + #isValidBytes(json: Uint8Array): boolean { + if (indexOf(json, this.#rawEncoding) === -1) { + return true + } + return false + } + + #concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array { const c = new Uint8Array(a.length + b.length) c.set(a, 0) c.set(b, a.length) @@ -227,7 +250,13 @@ Chrome's behavior: Chrome generally handles larger data sizes without this speci }) this.#validateResponse(response) const responseText = await response.text() + if (!this.#isValidString(responseText)) { + throw new ObjectLoaderRuntimeError('Invalid base object') + } const item = this.#processJson(this.#options.objectId, responseText) + if (!item.base) { + throw new ObjectLoaderRuntimeError('Invalid base object') + } item.size = 0 return item } diff --git a/packages/objectloader2/src/types/functions.spec.ts b/packages/objectloader2/src/types/functions.spec.ts index 3be9bf6da..f2a2a78fa 100644 --- a/packages/objectloader2/src/types/functions.spec.ts +++ b/packages/objectloader2/src/types/functions.spec.ts @@ -100,11 +100,15 @@ describe('take', () => { }) }) -describe('getQueryParameter', () => { +describe('getFeatureFlag', () => { describe('in a non-browser environment', () => { it('should return the default value', () => { expect(getFeatureFlag(ObjectLoader2Flags.USE_CACHE)).toBe('true') }) + + it('should return undefined when useDefault is false', () => { + expect(getFeatureFlag(ObjectLoader2Flags.USE_CACHE, false)).toBe(undefined) + }) }) describe('in a browser environment', () => { @@ -137,5 +141,15 @@ describe('getQueryParameter', () => { mockWindow.location.search = '' expect(getFeatureFlag(ObjectLoader2Flags.DEBUG)).toBe('false') }) + + it('should return undefined if useDefault is false and parameter is not in URL', () => { + mockWindow.location.search = '?otherparam=value' + expect(getFeatureFlag(ObjectLoader2Flags.DEBUG, false)).toBe(undefined) + }) + + it('should still return the parameter value from URL when useDefault is false', () => { + mockWindow.location.search = '?debug=custom' + expect(getFeatureFlag(ObjectLoader2Flags.DEBUG, false)).toBe('custom') + }) }) }) diff --git a/packages/objectloader2/src/types/functions.ts b/packages/objectloader2/src/types/functions.ts index 00709b6fa..042b93fea 100644 --- a/packages/objectloader2/src/types/functions.ts +++ b/packages/objectloader2/src/types/functions.ts @@ -60,14 +60,17 @@ const defaultValues: Record = { [ObjectLoader2Flags.USE_CACHE]: 'true' } -export function getFeatureFlag(paramName: ObjectLoader2Flags): string { +export function getFeatureFlag( + paramName: ObjectLoader2Flags, + useDefault: boolean = true +): string | undefined { // Check if the code is running in a browser environment 🌐 const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined' if (!isBrowser) { // If in Node.js or another server environment, return the default - return defaultValues[paramName] + return useDefault ? defaultValues[paramName] : undefined } // In a browser, parse the query string @@ -76,5 +79,40 @@ export function getFeatureFlag(paramName: ObjectLoader2Flags): string { // .get() returns the value, or null if it's not found. // The nullish coalescing operator (??) provides the default value // if the left-hand side is null or undefined. - return params.get(paramName) ?? defaultValues[paramName] + return params.get(paramName) ?? (useDefault ? defaultValues[paramName] : undefined) +} + +/** + * Finds the first index of a "needle" Uint8Array within a "haystack" Uint8Array. + * @param haystack The larger array to search within. + * @param needle The smaller array to search for. + * @param start The index to start searching from. Defaults to 0. + * @returns The starting index of the needle, or -1 if not found. + */ +export function indexOf( + haystack: Uint8Array, + needle: Uint8Array, + start: number = 0 +): number { + if (needle.length === 0) { + return 0 + } + + // The last possible starting position for a match + const limit = haystack.length - needle.length + + for (let i = start; i <= limit; i++) { + let foundMatch = true + for (let j = 0; j < needle.length; j++) { + if (haystack[i + j] !== needle[j]) { + foundMatch = false + break // Mismatch, break inner loop + } + } + if (foundMatch) { + return i // Found a full match at index i + } + } + + return -1 // No match found } diff --git a/packages/server/assets/core/typedefs/modelsAndVersions.graphql b/packages/server/assets/core/typedefs/modelsAndVersions.graphql index 4e9ec862b..e070b5b67 100644 --- a/packages/server/assets/core/typedefs/modelsAndVersions.graphql +++ b/packages/server/assets/core/typedefs/modelsAndVersions.graphql @@ -200,11 +200,12 @@ input MarkReceivedVersionInput { } type VersionMutations { - moveToModel(input: MoveVersionsInput!): Model! - delete(input: DeleteVersionsInput!): Boolean! - update(input: UpdateVersionInput!): Version! - create(input: CreateVersionInput!): Version! + moveToModel(input: MoveVersionsInput!): Model! @hasScope(scope: "streams:write") + delete(input: DeleteVersionsInput!): Boolean! @hasScope(scope: "streams:write") + update(input: UpdateVersionInput!): Version! @hasScope(scope: "streams:write") + create(input: CreateVersionInput!): Version! @hasScope(scope: "streams:write") markReceived(input: MarkReceivedVersionInput!): Boolean! + @hasScope(scope: "streams:read") } extend type Mutation { @@ -212,9 +213,7 @@ extend type Mutation { @hasServerRole(role: SERVER_GUEST) @hasScope(scope: "streams:write") - versionMutations: VersionMutations! - @hasServerRole(role: SERVER_GUEST) - @hasScope(scope: "streams:write") + versionMutations: VersionMutations! @hasServerRole(role: SERVER_GUEST) } type ModelsTreeItem { diff --git a/packages/server/assets/fileuploads/typedefs/fileuploads.graphql b/packages/server/assets/fileuploads/typedefs/fileuploads.graphql index e87baa18b..00883c81e 100644 --- a/packages/server/assets/fileuploads/typedefs/fileuploads.graphql +++ b/packages/server/assets/fileuploads/typedefs/fileuploads.graphql @@ -181,7 +181,7 @@ type FileUploadMutations { recording the provided status, and emitting the needed subscriptions. Mostly for internal service usage. """ - finishFileImport(input: FinishFileImportInput!): Boolean + finishFileImport(input: FinishFileImportInput!): Boolean! @hasScope(scope: "streams:write") } diff --git a/packages/server/modules/activitystream/repositories/index.ts b/packages/server/modules/activitystream/repositories/index.ts index 6f9d326b1..5acc53391 100644 --- a/packages/server/modules/activitystream/repositories/index.ts +++ b/packages/server/modules/activitystream/repositories/index.ts @@ -242,7 +242,7 @@ export const saveStreamActivityFactory = actionType, // "commit_receive" userId, // populated by the api info: JSON.stringify(info), // can be anything with conventions! (TBD) - message // something human understandable for frontend purposes mostly + message: message?.slice(0, 255) ?? message // something human understandable for frontend purposes mostly } await tables diff --git a/packages/server/modules/activitystream/tests/integration/repository/activityStream.spec.ts b/packages/server/modules/activitystream/tests/integration/repository/activityStream.spec.ts new file mode 100644 index 000000000..7452117b9 --- /dev/null +++ b/packages/server/modules/activitystream/tests/integration/repository/activityStream.spec.ts @@ -0,0 +1,77 @@ +import { db } from '@/db/knex' +import { saveStreamActivityFactory } from '@/modules/activitystream/repositories' +import { StreamActivity } from '@/modules/core/dbSchema' +import { expect } from 'chai' +import cryptoRandomString from 'crypto-random-string' + +describe('Stream activity repository @activitystream', () => { + const saveStreamActivity = saveStreamActivityFactory({ db }) + const resourceId = cryptoRandomString({ length: 10 }) + const userId = cryptoRandomString({ length: 10 }) + const exampleActivity = { + streamId: null, + resourceType: 'user' as const, + resourceId, + userId, + actionType: 'user_update' as const, + info: { + new: { + name: 'user', + field: 'a' + }, + old: { + name: 'user2', + field: 'b' + } + }, + message: 'User plan updated' + } + + it('stores an activity', async () => { + await saveStreamActivity(exampleActivity) + + const activity = await db + .table(StreamActivity.name) + .select(StreamActivity.cols) + .where({ + userId, + resourceId, + actionType: 'user_update' + }) + .first() + + expect(activity).to.nested.include({ + resourceType: 'user', + resourceId, + userId, + actionType: 'user_update', + message: 'User plan updated' + }) + expect(activity).to.have.a.property('time').that.is.a('Date') + }) + + it('trims the message in case its too long', async () => { + await saveStreamActivity({ + ...exampleActivity, + message: cryptoRandomString({ length: 1000 }) + }) + + const activity = await db + .table(StreamActivity.name) + .select(StreamActivity.cols) + .where({ + userId, + resourceId, + actionType: 'user_update' + }) + .first() + + expect(activity).to.nested.include({ + resourceType: 'user', + resourceId, + userId, + actionType: 'user_update' + }) + expect(activity).to.have.a.property('time').that.is.a('Date') + }) +}) diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index f6cad3011..702f317d0 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -1128,7 +1128,7 @@ export type FileUploadMutations = { * recording the provided status, and emitting the needed subscriptions. * Mostly for internal service usage. */ - finishFileImport?: Maybe; + finishFileImport: Scalars['Boolean']['output']; /** * Generate a pre-signed url to which a file can be uploaded. * After uploading the file, call mutation startFileImport to register the completed upload. @@ -6659,7 +6659,7 @@ export type FileUploadCollectionResolvers = { - finishFileImport?: Resolver, ParentType, ContextType, RequireFields>; + finishFileImport?: Resolver>; generateUploadUrl?: Resolver>; startFileImport?: Resolver>; __isTypeOf?: IsTypeOfResolverFn; diff --git a/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts b/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts index 9d27a91f5..a19251932 100644 --- a/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts +++ b/packages/server/modules/fileuploads/graph/resolvers/fileUploads.ts @@ -302,7 +302,7 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = { jobResult }) - return null + return true } } diff --git a/packages/server/modules/fileuploads/index.ts b/packages/server/modules/fileuploads/index.ts index 4e72d01c2..9932200b2 100644 --- a/packages/server/modules/fileuploads/index.ts +++ b/packages/server/modules/fileuploads/index.ts @@ -147,8 +147,8 @@ export const init: SpeckleModule['init'] = async ({ const queueDb = connectionUri ? configureClient({ postgres: { connectionUri } }).public : db - const queueInits = [ - initializePostgresQueue({ + const requestQueues = [ + await initializePostgresQueue({ label: 'ifc', supportedFileTypes: ['ifc'], db: queueDb @@ -156,24 +156,19 @@ export const init: SpeckleModule['init'] = async ({ ] if (FF_RHINO_FILE_IMPORTER_ENABLED) { moduleLogger.info('🦏 Rhino File Importer is ENABLED') - const connectionUri = getFileImporterQueuePostgresUrl() if (!connectionUri) throw new MisconfiguredEnvironmentError( 'Need a dedicated queue for Rhino based fileimports' ) - const rhinoQueueDb = configureClient({ postgres: { connectionUri } }) - queueInits.push( - initializePostgresQueue({ + requestQueues.push( + await initializePostgresQueue({ label: 'rhino', supportedFileTypes: ['obj', 'stl', 'skp'], // using public here, as the private uri is not applicable here - db: rhinoQueueDb.public + db: queueDb }) ) } - // no need to store the queue refs here for now - const requestQueues = await Promise.all(queueInits) - //stick to the bull queue based mechanism by default ;({ observeResult } = initializeMetrics({ registers: [metricsRegister], requestQueues diff --git a/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts b/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts index a69f894a2..182eb1810 100644 --- a/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts +++ b/packages/server/modules/gatekeeper/clients/checkout/createCheckoutSession.ts @@ -45,6 +45,9 @@ export const createCheckoutSessionFactory = success_url: `${resultUrl.toString()}&payment_status=success&session_id={CHECKOUT_SESSION_ID}`, cancel_url, + automatic_tax: { + enabled: true + }, tax_id_collection: { enabled: true } diff --git a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts index 89d082295..03a199986 100644 --- a/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts +++ b/packages/viewer/src/modules/loaders/Speckle/SpeckleLoader.ts @@ -192,7 +192,7 @@ export class SpeckleLoader extends Loader { } private progressListen(): void { - if (getFeatureFlag(ObjectLoader2Flags.DEBUG) !== 'true') { + if (getFeatureFlag(ObjectLoader2Flags.DEBUG, false) !== 'true') { return } diff --git a/yarn.lock b/yarn.lock index 5453133d0..2885851bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16158,7 +16158,6 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^7.12.0" "@typescript-eslint/parser": "npm:^7.12.0" "@vitest/ui": "npm:^3.0.9" - dexie: "npm:^4.0.11" eslint: "npm:^9.4.0" eslint-config-prettier: "npm:^9.1.0" fake-indexeddb: "npm:^6.0.0"