From 139258dd59b668cf584d2a079ba3964e088243e3 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 29 Aug 2025 10:09:59 +0000 Subject: [PATCH 001/219] use src layout --- Dockerfile | 2 +- docker-compose.yaml | 2 +- pyproject.toml | 1 + {datajoint => src/datajoint}/__init__.py | 0 {datajoint => src/datajoint}/admin.py | 0 {datajoint => src/datajoint}/attribute_adapter.py | 0 {datajoint => src/datajoint}/autopopulate.py | 0 {datajoint => src/datajoint}/blob.py | 0 {datajoint => src/datajoint}/cli.py | 0 {datajoint => src/datajoint}/condition.py | 0 {datajoint => src/datajoint}/connection.py | 0 {datajoint => src/datajoint}/declare.py | 0 {datajoint => src/datajoint}/dependencies.py | 0 {datajoint => src/datajoint}/diagram.py | 0 {datajoint => src/datajoint}/errors.py | 0 {datajoint => src/datajoint}/expression.py | 0 {datajoint => src/datajoint}/external.py | 0 {datajoint => src/datajoint}/fetch.py | 0 {datajoint => src/datajoint}/hash.py | 0 {datajoint => src/datajoint}/heading.py | 0 {datajoint => src/datajoint}/jobs.py | 0 {datajoint => src/datajoint}/logging.py | 0 {datajoint => src/datajoint}/preview.py | 0 {datajoint => src/datajoint}/s3.py | 0 {datajoint => src/datajoint}/schemas.py | 0 {datajoint => src/datajoint}/settings.py | 0 {datajoint => src/datajoint}/table.py | 0 {datajoint => src/datajoint}/user_tables.py | 0 {datajoint => src/datajoint}/utils.py | 0 {datajoint => src/datajoint}/version.py | 0 30 files changed, 3 insertions(+), 2 deletions(-) rename {datajoint => src/datajoint}/__init__.py (100%) rename {datajoint => src/datajoint}/admin.py (100%) rename {datajoint => src/datajoint}/attribute_adapter.py (100%) rename {datajoint => src/datajoint}/autopopulate.py (100%) rename {datajoint => src/datajoint}/blob.py (100%) rename {datajoint => src/datajoint}/cli.py (100%) rename {datajoint => src/datajoint}/condition.py (100%) rename {datajoint => src/datajoint}/connection.py (100%) rename {datajoint => src/datajoint}/declare.py (100%) rename {datajoint => src/datajoint}/dependencies.py (100%) rename {datajoint => src/datajoint}/diagram.py (100%) rename {datajoint => src/datajoint}/errors.py (100%) rename {datajoint => src/datajoint}/expression.py (100%) rename {datajoint => src/datajoint}/external.py (100%) rename {datajoint => src/datajoint}/fetch.py (100%) rename {datajoint => src/datajoint}/hash.py (100%) rename {datajoint => src/datajoint}/heading.py (100%) rename {datajoint => src/datajoint}/jobs.py (100%) rename {datajoint => src/datajoint}/logging.py (100%) rename {datajoint => src/datajoint}/preview.py (100%) rename {datajoint => src/datajoint}/s3.py (100%) rename {datajoint => src/datajoint}/schemas.py (100%) rename {datajoint => src/datajoint}/settings.py (100%) rename {datajoint => src/datajoint}/table.py (100%) rename {datajoint => src/datajoint}/user_tables.py (100%) rename {datajoint => src/datajoint}/utils.py (100%) rename {datajoint => src/datajoint}/version.py (100%) diff --git a/Dockerfile b/Dockerfile index 0d727f6b4..780e1c540 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN ${CONDA_BIN} install --no-pin -qq -y -n base -c conda-forge \ ENV PATH="$PATH:/home/mambauser/.local/bin" COPY --chown=${HOST_UID:-1000}:mambauser ./pyproject.toml ./README.md ./LICENSE.txt /main/ -COPY --chown=${HOST_UID:-1000}:mambauser ./datajoint /main/datajoint +COPY --chown=${HOST_UID:-1000}:mambauser ./src/datajoint /main/src/datajoint VOLUME /src WORKDIR /src diff --git a/docker-compose.yaml b/docker-compose.yaml index d09d06d49..40b211756 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -40,7 +40,7 @@ services: context: . dockerfile: Dockerfile args: - PY_VER: ${PY_VER:-3.8} + PY_VER: ${PY_VER:-3.9} HOST_UID: ${HOST_UID:-1000} depends_on: db: diff --git a/pyproject.toml b/pyproject.toml index b98361fe8..b1d672af8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ profile = "black" [tool.setuptools] packages = ["datajoint"] +package-dir = {"" = "src"} [tool.setuptools.dynamic] version = { attr = "datajoint.version.__version__"} diff --git a/datajoint/__init__.py b/src/datajoint/__init__.py similarity index 100% rename from datajoint/__init__.py rename to src/datajoint/__init__.py diff --git a/datajoint/admin.py b/src/datajoint/admin.py similarity index 100% rename from datajoint/admin.py rename to src/datajoint/admin.py diff --git a/datajoint/attribute_adapter.py b/src/datajoint/attribute_adapter.py similarity index 100% rename from datajoint/attribute_adapter.py rename to src/datajoint/attribute_adapter.py diff --git a/datajoint/autopopulate.py b/src/datajoint/autopopulate.py similarity index 100% rename from datajoint/autopopulate.py rename to src/datajoint/autopopulate.py diff --git a/datajoint/blob.py b/src/datajoint/blob.py similarity index 100% rename from datajoint/blob.py rename to src/datajoint/blob.py diff --git a/datajoint/cli.py b/src/datajoint/cli.py similarity index 100% rename from datajoint/cli.py rename to src/datajoint/cli.py diff --git a/datajoint/condition.py b/src/datajoint/condition.py similarity index 100% rename from datajoint/condition.py rename to src/datajoint/condition.py diff --git a/datajoint/connection.py b/src/datajoint/connection.py similarity index 100% rename from datajoint/connection.py rename to src/datajoint/connection.py diff --git a/datajoint/declare.py b/src/datajoint/declare.py similarity index 100% rename from datajoint/declare.py rename to src/datajoint/declare.py diff --git a/datajoint/dependencies.py b/src/datajoint/dependencies.py similarity index 100% rename from datajoint/dependencies.py rename to src/datajoint/dependencies.py diff --git a/datajoint/diagram.py b/src/datajoint/diagram.py similarity index 100% rename from datajoint/diagram.py rename to src/datajoint/diagram.py diff --git a/datajoint/errors.py b/src/datajoint/errors.py similarity index 100% rename from datajoint/errors.py rename to src/datajoint/errors.py diff --git a/datajoint/expression.py b/src/datajoint/expression.py similarity index 100% rename from datajoint/expression.py rename to src/datajoint/expression.py diff --git a/datajoint/external.py b/src/datajoint/external.py similarity index 100% rename from datajoint/external.py rename to src/datajoint/external.py diff --git a/datajoint/fetch.py b/src/datajoint/fetch.py similarity index 100% rename from datajoint/fetch.py rename to src/datajoint/fetch.py diff --git a/datajoint/hash.py b/src/datajoint/hash.py similarity index 100% rename from datajoint/hash.py rename to src/datajoint/hash.py diff --git a/datajoint/heading.py b/src/datajoint/heading.py similarity index 100% rename from datajoint/heading.py rename to src/datajoint/heading.py diff --git a/datajoint/jobs.py b/src/datajoint/jobs.py similarity index 100% rename from datajoint/jobs.py rename to src/datajoint/jobs.py diff --git a/datajoint/logging.py b/src/datajoint/logging.py similarity index 100% rename from datajoint/logging.py rename to src/datajoint/logging.py diff --git a/datajoint/preview.py b/src/datajoint/preview.py similarity index 100% rename from datajoint/preview.py rename to src/datajoint/preview.py diff --git a/datajoint/s3.py b/src/datajoint/s3.py similarity index 100% rename from datajoint/s3.py rename to src/datajoint/s3.py diff --git a/datajoint/schemas.py b/src/datajoint/schemas.py similarity index 100% rename from datajoint/schemas.py rename to src/datajoint/schemas.py diff --git a/datajoint/settings.py b/src/datajoint/settings.py similarity index 100% rename from datajoint/settings.py rename to src/datajoint/settings.py diff --git a/datajoint/table.py b/src/datajoint/table.py similarity index 100% rename from datajoint/table.py rename to src/datajoint/table.py diff --git a/datajoint/user_tables.py b/src/datajoint/user_tables.py similarity index 100% rename from datajoint/user_tables.py rename to src/datajoint/user_tables.py diff --git a/datajoint/utils.py b/src/datajoint/utils.py similarity index 100% rename from datajoint/utils.py rename to src/datajoint/utils.py diff --git a/datajoint/version.py b/src/datajoint/version.py similarity index 100% rename from datajoint/version.py rename to src/datajoint/version.py From ebeab884d906be91402eef15487b45c0f16ea055 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 14:12:44 +0200 Subject: [PATCH 002/219] use pytest to manage docker container startup for tests --- .github/workflows/test.yaml | 6 +- docker-compose.yaml | 3 +- pyproject.toml | 8 +- tests/conftest.py | 255 +++++++++++++++++++++++++++++++++++- tests/test_json.py | 14 +- 5 files changed, 271 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 196ddec22..893e8cf07 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,9 +34,7 @@ jobs: python-version: ${{matrix.py_ver}} - name: Integration test env: - PY_VER: ${{matrix.py_ver}} MYSQL_VER: ${{matrix.mysql_ver}} - # taking default variables set in docker-compose.yaml to sync with local test run: | - export HOST_UID=$(id -u) - docker compose --profile test up --quiet-pull --build --exit-code-from djtest djtest + pip install -e ".[test]" + pytest --cov-report term-missing --cov=datajoint tests diff --git a/docker-compose.yaml b/docker-compose.yaml index 40b211756..4c470c3f8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,5 @@ -# HOST_UID=$(id -u) PY_VER=3.11 DJ_VERSION=$(grep -oP '\d+\.\d+\.\d+' datajoint/version.py) docker compose --profile test up --build --exit-code-from djtest djtest +# Development environment with MySQL and MinIO services +# To run tests: pytest --cov-report term-missing --cov=datajoint tests services: db: image: datajoint/mysql:${MYSQL_VER:-8.0} diff --git a/pyproject.toml b/pyproject.toml index b1d672af8..7da02f209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "urllib3", "setuptools", ] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.11,<4.0" authors = [ {name = "Dimitri Yatsenko", email = "dimitri@datajoint.com"}, {name = "Thinh Nguyen", email = "thinh@datajoint.com"}, @@ -78,11 +78,15 @@ Repository = "https://github.com/datajoint/datajoint-python" dj = "datajoint.cli:cli" datajoint = "datajoint.cli:cli" -[project.optional-dependencies] +[dependency-groups] test = [ "pytest", "pytest-cov", + "docker", + "requests", ] + +[project.optional-dependencies] dev = [ "pre-commit", "black==24.2.0", diff --git a/tests/conftest.py b/tests/conftest.py index 88d55e32f..80bd3f446 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,20 @@ +import atexit import json +import logging import os import shutil +import signal +import time from os import environ, remove from pathlib import Path from typing import Dict, List import certifi +import docker import minio import networkx as nx import pytest +import requests import urllib3 from packaging import version @@ -24,6 +30,240 @@ from . import schema_uuid as schema_uuid_module +# Configure logging for container management +logger = logging.getLogger(__name__) + + + + +# Global container registry for cleanup +_active_containers = set() +_docker_client = None + + +def _get_docker_client(): + """Get or create docker client""" + global _docker_client + if _docker_client is None: + _docker_client = docker.from_env() + return _docker_client + + +def _cleanup_containers(): + """Clean up any remaining containers""" + if _active_containers: + logger.info(f"Emergency cleanup: {len(_active_containers)} containers to clean up") + try: + client = _get_docker_client() + for container_id in list(_active_containers): + try: + container = client.containers.get(container_id) + container.remove(force=True) + logger.info(f"Emergency cleanup: removed container {container_id[:12]}") + except docker.errors.NotFound: + logger.debug(f"Container {container_id[:12]} already removed") + except Exception as e: + logger.error(f"Error cleaning up container {container_id[:12]}: {e}") + finally: + _active_containers.discard(container_id) + except Exception as e: + logger.error(f"Error during emergency cleanup: {e}") + else: + logger.debug("No containers to clean up") + + +def _register_container(container): + """Register a container for cleanup""" + _active_containers.add(container.id) + logger.debug(f"Registered container {container.id[:12]} for cleanup") + + +def _unregister_container(container): + """Unregister a container from cleanup""" + _active_containers.discard(container.id) + logger.debug(f"Unregistered container {container.id[:12]} from cleanup") + + +# Register cleanup functions +atexit.register(_cleanup_containers) + + +def _signal_handler(signum, frame): + """Handle signals to ensure container cleanup""" + logger.warning(f"Received signal {signum}, performing emergency container cleanup...") + _cleanup_containers() + + # Restore default signal handler and re-raise the signal + # This allows pytest to handle the cancellation normally + signal.signal(signum, signal.SIG_DFL) + os.kill(os.getpid(), signum) + + +# Register signal handlers for graceful cleanup, but only for non-interactive scenarios +# In pytest, we'll rely on fixture teardown and atexit handlers primarily +try: + import pytest + # If we're here, pytest is available, so only register SIGTERM (for CI/batch scenarios) + signal.signal(signal.SIGTERM, _signal_handler) + # Don't intercept SIGINT (Ctrl+C) to allow pytest's normal cancellation behavior +except ImportError: + # If pytest isn't available, register both handlers + signal.signal(signal.SIGINT, _signal_handler) + signal.signal(signal.SIGTERM, _signal_handler) + + +@pytest.fixture(scope="session") +def docker_client(): + """Docker client for managing containers.""" + return _get_docker_client() + + +@pytest.fixture(scope="session") +def mysql_container(docker_client): + """Start MySQL container and wait for it to be healthy.""" + mysql_ver = os.environ.get("MYSQL_VER", "8.0") + container_name = f"datajoint_test_mysql_{os.getpid()}" + + logger.info(f"Starting MySQL container {container_name} with version {mysql_ver}") + + # Remove existing container if it exists + try: + existing = docker_client.containers.get(container_name) + logger.info(f"Removing existing MySQL container {container_name}") + existing.remove(force=True) + except docker.errors.NotFound: + logger.debug(f"No existing MySQL container {container_name} found") + + # Start MySQL container + container = docker_client.containers.run( + f"datajoint/mysql:{mysql_ver}", + name=container_name, + environment={ + "MYSQL_ROOT_PASSWORD": "password" + }, + command="mysqld --default-authentication-plugin=mysql_native_password", + ports={"3306/tcp": None}, # Let Docker assign random port + detach=True, + remove=True, + healthcheck={ + "test": ["CMD", "mysqladmin", "ping", "-h", "localhost"], + "timeout": 30000000000, # 30s in nanoseconds + "retries": 5, + "interval": 15000000000, # 15s in nanoseconds + } + ) + + # Register container for cleanup + _register_container(container) + logger.info(f"MySQL container {container_name} started with ID {container.id[:12]}") + + # Wait for health check + max_wait = 120 # 2 minutes + start_time = time.time() + logger.info(f"Waiting for MySQL container {container_name} to become healthy (max {max_wait}s)") + + while time.time() - start_time < max_wait: + container.reload() + health_status = container.attrs["State"]["Health"]["Status"] + logger.debug(f"MySQL container {container_name} health status: {health_status}") + if health_status == "healthy": + break + time.sleep(2) + else: + logger.error(f"MySQL container {container_name} failed to become healthy within {max_wait}s") + container.remove(force=True) + raise RuntimeError("MySQL container failed to become healthy") + + # Get the mapped port + port_info = container.attrs["NetworkSettings"]["Ports"]["3306/tcp"] + if port_info: + host_port = port_info[0]["HostPort"] + logger.info(f"MySQL container {container_name} is healthy and accessible on localhost:{host_port}") + else: + raise RuntimeError("Failed to get MySQL port mapping") + + yield container, "localhost", int(host_port) + + # Cleanup + logger.info(f"Cleaning up MySQL container {container_name}") + _unregister_container(container) + container.remove(force=True) + logger.info(f"MySQL container {container_name} removed") + + +@pytest.fixture(scope="session") +def minio_container(docker_client): + """Start MinIO container and wait for it to be healthy.""" + minio_ver = os.environ.get("MINIO_VER", "RELEASE.2025-02-28T09-55-16Z") + container_name = f"datajoint_test_minio_{os.getpid()}" + + logger.info(f"Starting MinIO container {container_name} with version {minio_ver}") + + # Remove existing container if it exists + try: + existing = docker_client.containers.get(container_name) + logger.info(f"Removing existing MinIO container {container_name}") + existing.remove(force=True) + except docker.errors.NotFound: + logger.debug(f"No existing MinIO container {container_name} found") + + # Start MinIO container + container = docker_client.containers.run( + f"minio/minio:{minio_ver}", + name=container_name, + environment={ + "MINIO_ACCESS_KEY": "datajoint", + "MINIO_SECRET_KEY": "datajoint" + }, + command=['server', '--address', ':9000', '/data'], + ports={"9000/tcp": None}, # Let Docker assign random port + detach=True, + remove=True + ) + + # Register container for cleanup + _register_container(container) + logger.info(f"MinIO container {container_name} started with ID {container.id[:12]}") + + # Get the mapped port + container.reload() + port_info = container.attrs["NetworkSettings"]["Ports"]["9000/tcp"] + if port_info: + host_port = port_info[0]["HostPort"] + logger.info(f"MinIO container {container_name} mapped to localhost:{host_port}") + else: + raise RuntimeError("Failed to get MinIO port mapping") + + # Wait for MinIO to be ready + minio_url = f"http://localhost:{host_port}" + max_wait = 60 + start_time = time.time() + logger.info(f"Waiting for MinIO container {container_name} to become ready (max {max_wait}s)") + + while time.time() - start_time < max_wait: + try: + response = requests.get(f"{minio_url}/minio/health/live", timeout=5) + if response.status_code == 200: + logger.info(f"MinIO container {container_name} is ready and accessible at {minio_url}") + break + except requests.exceptions.RequestException: + logger.debug(f"MinIO container {container_name} not ready yet, retrying...") + pass + time.sleep(2) + else: + logger.error(f"MinIO container {container_name} failed to become ready within {max_wait}s") + container.remove(force=True) + raise RuntimeError("MinIO container failed to become ready") + + yield container, "localhost", int(host_port) + + # Cleanup + logger.info(f"Cleaning up MinIO container {container_name}") + _unregister_container(container) + container.remove(force=True) + logger.info(f"MinIO container {container_name} removed") + + @pytest.fixture(scope="session") def prefix(): return os.environ.get("DJ_TEST_DB_PREFIX", "djtest") @@ -56,18 +296,20 @@ def enable_filepath_feature(monkeypatch): @pytest.fixture(scope="session") -def db_creds_test() -> Dict: +def db_creds_test(mysql_container) -> Dict: + _, host, port = mysql_container return dict( - host=os.getenv("DJ_TEST_HOST", "db"), + host=f"{host}:{port}", user=os.getenv("DJ_TEST_USER", "datajoint"), password=os.getenv("DJ_TEST_PASSWORD", "datajoint"), ) @pytest.fixture(scope="session") -def db_creds_root() -> Dict: +def db_creds_root(mysql_container) -> Dict: + _, host, port = mysql_container return dict( - host=os.getenv("DJ_HOST", "db"), + host=f"{host}:{port}", user=os.getenv("DJ_USER", "root"), password=os.getenv("DJ_PASS", "password"), ) @@ -190,9 +432,10 @@ def connection_test(connection_root, prefix, db_creds_test): @pytest.fixture(scope="session") -def s3_creds() -> Dict: +def s3_creds(minio_container) -> Dict: + _, host, port = minio_container return dict( - endpoint=os.environ.get("S3_ENDPOINT", "minio:9000"), + endpoint=f"{host}:{port}", access_key=os.environ.get("S3_ACCESS_KEY", "datajoint"), secret_key=os.environ.get("S3_SECRET_KEY", "datajoint"), bucket=os.environ.get("S3_BUCKET", "datajoint.test"), diff --git a/tests/test_json.py b/tests/test_json.py index 0a819b99e..21d03c86e 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -7,8 +7,18 @@ import datajoint as dj from datajoint.declare import declare -if Version(dj.conn().query("select @@version;").fetchone()[0]) < Version("8.0.0"): - pytest.skip("These tests require MySQL >= v8.0.0", allow_module_level=True) + +def mysql_version_check(connection): + """Check if MySQL version is >= 8.0.0""" + version_str = connection.query("select @@version;").fetchone()[0] + if Version(version_str) < Version("8.0.0"): + pytest.skip("These tests require MySQL >= v8.0.0") + + +@pytest.fixture(scope="module", autouse=True) +def check_mysql_version(connection_root): + """Automatically check MySQL version for all tests in this module""" + mysql_version_check(connection_root) class Team(dj.Lookup): From 718e21933bf5009532127645f69e49fe7f0b708e Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 14:56:51 +0200 Subject: [PATCH 003/219] fix environment variable mismatch --- pyproject.toml | 13 +++++++++++++ tests/conftest.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 7da02f209..d77af5e9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ datajoint = "datajoint.cli:cli" test = [ "pytest", "pytest-cov", + "pytest-env", "docker", "requests", ] @@ -107,3 +108,15 @@ package-dir = {"" = "src"} [tool.setuptools.dynamic] version = { attr = "datajoint.version.__version__"} + +[tool.pytest_env] +# Default values - pytest fixtures will override with actual container details +DJ_USER="root" +DJ_PASS="password" +DJ_TEST_USER="datajoint" +DJ_TEST_PASSWORD="datajoint" +S3_ACCESS_KEY="datajoint" +S3_SECRET_KEY="datajoint" +S3_BUCKET="datajoint.test" +PYTHON_USER="dja" +JUPYTER_PASSWORD="datajoint" \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 80bd3f446..5238fa84d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -298,6 +298,14 @@ def enable_filepath_feature(monkeypatch): @pytest.fixture(scope="session") def db_creds_test(mysql_container) -> Dict: _, host, port = mysql_container + # Set environment variables for DataJoint at module level + os.environ["DJ_TEST_HOST"] = host + os.environ["DJ_TEST_PORT"] = str(port) + + # Also update DataJoint's test configuration directly + dj.config["database.test.host"] = host + dj.config["database.test.port"] = port + return dict( host=f"{host}:{port}", user=os.getenv("DJ_TEST_USER", "datajoint"), @@ -308,6 +316,14 @@ def db_creds_test(mysql_container) -> Dict: @pytest.fixture(scope="session") def db_creds_root(mysql_container) -> Dict: _, host, port = mysql_container + # Set environment variables for DataJoint at module level + os.environ["DJ_HOST"] = host + os.environ["DJ_PORT"] = str(port) + + # Also update DataJoint's configuration directly + dj.config["database.host"] = host + dj.config["database.port"] = port + return dict( host=f"{host}:{port}", user=os.getenv("DJ_USER", "root"), @@ -434,6 +450,8 @@ def connection_test(connection_root, prefix, db_creds_test): @pytest.fixture(scope="session") def s3_creds(minio_container) -> Dict: _, host, port = minio_container + # Set environment variable for S3 endpoint at module level + os.environ["S3_ENDPOINT"] = f"{host}:{port}" return dict( endpoint=f"{host}:{port}", access_key=os.environ.get("S3_ACCESS_KEY", "datajoint"), From 1252addad7316e6cc5df2f10bca65954307dfef6 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 15:42:39 +0200 Subject: [PATCH 004/219] add database.port to settings.py, and update conftest --- src/datajoint/settings.py | 10 ++++++++++ tests/conftest.py | 33 +++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 30b206f99..c6c4dc550 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -278,6 +278,7 @@ def __setitem__(self, key, value): "database.host", "database.user", "database.password", + "database.port", "external.aws_access_key_id", "external.aws_secret_access_key", "loglevel", @@ -288,6 +289,7 @@ def __setitem__(self, key, value): "DJ_HOST", "DJ_USER", "DJ_PASS", + "DJ_PORT", "DJ_AWS_ACCESS_KEY_ID", "DJ_AWS_SECRET_ACCESS_KEY", "DJ_LOG_LEVEL", @@ -296,6 +298,14 @@ def __setitem__(self, key, value): ) if v is not None } + +# Convert DJ_PORT from string to int if present +if "database.port" in mapping and mapping["database.port"] is not None: + try: + mapping["database.port"] = int(mapping["database.port"]) + except ValueError: + logger.warning(f"Invalid DJ_PORT value: {mapping['database.port']}, using default port 3306") + del mapping["database.port"] if mapping: logger.info(f"Overloaded settings {tuple(mapping)} from environment variables.") config.update(mapping) diff --git a/tests/conftest.py b/tests/conftest.py index 5238fa84d..c1bea5b78 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,6 +34,18 @@ logger = logging.getLogger(__name__) +def pytest_sessionstart(session): + """Called after the Session object has been created and configured.""" + # This runs very early, before most fixtures, but we don't have container info yet + pass + + +def pytest_configure(config): + """Called after command line options have been parsed.""" + # This runs before pytest_sessionstart but still too early for containers + pass + + # Global container registry for cleanup @@ -313,17 +325,30 @@ def db_creds_test(mysql_container) -> Dict: ) -@pytest.fixture(scope="session") -def db_creds_root(mysql_container) -> Dict: +@pytest.fixture(scope="session", autouse=True) +def configure_datajoint_for_containers(mysql_container): + """Configure DataJoint to use pytest-managed containers. Runs automatically for all tests.""" _, host, port = mysql_container - # Set environment variables for DataJoint at module level + + # Set environment variables FIRST - these will be inherited by subprocesses + logger.info(f"🔧 Setting environment: DJ_HOST={host}, DJ_PORT={port}") os.environ["DJ_HOST"] = host os.environ["DJ_PORT"] = str(port) - # Also update DataJoint's configuration directly + # Verify the environment variables were set + logger.info(f"🔧 Environment after setting: DJ_HOST={os.environ.get('DJ_HOST')}, DJ_PORT={os.environ.get('DJ_PORT')}") + + # Also update DataJoint's configuration directly for in-process connections dj.config["database.host"] = host dj.config["database.port"] = port + logger.info(f"🔧 Configured DataJoint to use MySQL container at {host}:{port}") + return host, port # Return values so other fixtures can use them + + +@pytest.fixture(scope="session") +def db_creds_root(mysql_container) -> Dict: + _, host, port = mysql_container return dict( host=f"{host}:{port}", user=os.getenv("DJ_USER", "root"), From fecbb83f4fe725440e2b3af14c3aaa175674d0d8 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 17:27:41 +0200 Subject: [PATCH 005/219] revert python version floor increment --- pyproject.toml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d77af5e9a..a8deffa40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "urllib3", "setuptools", ] -requires-python = ">=3.11,<4.0" +requires-python = ">=3.9,<4.0" authors = [ {name = "Dimitri Yatsenko", email = "dimitri@datajoint.com"}, {name = "Thinh Nguyen", email = "thinh@datajoint.com"}, @@ -85,6 +85,7 @@ test = [ "pytest-env", "docker", "requests", + "graphviz" ] [project.optional-dependencies] @@ -119,4 +120,22 @@ S3_ACCESS_KEY="datajoint" S3_SECRET_KEY="datajoint" S3_BUCKET="datajoint.test" PYTHON_USER="dja" -JUPYTER_PASSWORD="datajoint" \ No newline at end of file +JUPYTER_PASSWORD="datajoint" + + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.pypi-dependencies] +datajoint = { path = ".", editable = true } + +[tool.pixi.environments] +default = { solve-group = "default" } +dev = { features = ["dev"], solve-group = "default" } +test = { features = ["test"], solve-group = "default" } + +[tool.pixi.tasks] + +[tool.pixi.dependencies] +graphviz = ">=13.1.2,<14" From 76aaf5e1251926610d59f8a9a19943d8be713200 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 17:28:10 +0200 Subject: [PATCH 006/219] use normal healthcheck intervals --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c1bea5b78..d0f31bb6a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,9 +159,9 @@ def mysql_container(docker_client): remove=True, healthcheck={ "test": ["CMD", "mysqladmin", "ping", "-h", "localhost"], - "timeout": 30000000000, # 30s in nanoseconds + "timeout": 30, "retries": 5, - "interval": 15000000000, # 15s in nanoseconds + "interval": 15, } ) From c68f1df0a2e0926411f5f03328de5c2fb01948eb Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 17:37:30 +0200 Subject: [PATCH 007/219] revert change to healthcheck, because the nanoseconds were correct --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d0f31bb6a..c1bea5b78 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,9 +159,9 @@ def mysql_container(docker_client): remove=True, healthcheck={ "test": ["CMD", "mysqladmin", "ping", "-h", "localhost"], - "timeout": 30, + "timeout": 30000000000, # 30s in nanoseconds "retries": 5, - "interval": 15, + "interval": 15000000000, # 15s in nanoseconds } ) From 3a34d828079281e99d5d5fa19ad9d870780ff8da Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 17:37:39 +0200 Subject: [PATCH 008/219] update gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index f506fcb59..8de8ad131 100644 --- a/.gitignore +++ b/.gitignore @@ -185,3 +185,5 @@ cython_debug/ dj_local_conf.json *.env !.vscode/launch.json +# pixi environments +.pixi From f73d7c7f758231949ec938a22b43f4e4d2149461 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 17:38:33 +0200 Subject: [PATCH 009/219] add pixi lockfile --- pixi.lock | 2712 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2712 insertions(+) create mode 100644 pixi.lock diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 000000000..6a7cd0202 --- /dev/null +++ b/pixi.lock @@ -0,0 +1,2712 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.1-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.0-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + - pypi: ./ + dev: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.1-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.0-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + - pypi: ./ + test: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.1-h2b0a6b4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.0-h15599e2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + - pypi: ./ +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_1.conda + sha256: f52307d3ff839bf4a001cb14b3944f169e46e37982a97c3d52cbf48a0cfe2327 + md5: 388097ca1f27fc28e0ef1986dd311891 + depends: + - __unix + - hicolor-icon-theme + - librsvg + license: LGPL-3.0-or-later OR CC-BY-SA-3.0 + license_family: LGPL + purls: [] + size: 621553 + timestamp: 1755882037787 +- pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + name: argon2-cffi + version: 25.1.0 + sha256: fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + requires_dist: + - argon2-cffi-bindings + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + name: asttokens + version: 3.0.0 + sha256: e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2 + requires_dist: + - astroid>=2,<4 ; extra == 'astroid' + - astroid>=2,<4 ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-xdist ; extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + sha256: 26ab9386e80bf196e51ebe005da77d57decf6d989b4f34d96130560bc133479c + md5: 6b889f174df1e0f816276ae69281af4d + depends: + - at-spi2-core >=2.40.0,<2.41.0a0 + - atk-1.0 >=2.36.0 + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.1,<3.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 339899 + timestamp: 1619122953439 +- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + sha256: c4f9b66bd94c40d8f1ce1fad2d8b46534bdefda0c86e3337b28f6c25779f258d + md5: 8cb2fc4cd6cc63f1369cfa318f581cc3 + depends: + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.3,<3.0a0 + - xorg-libx11 + - xorg-libxi + - xorg-libxtst + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 658390 + timestamp: 1625848454791 +- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + sha256: df682395d05050cd1222740a42a551281210726a67447e5258968dd55854302e + md5: f730d54ba9cd543666d7220c9f7ed563 + depends: + - libgcc-ng >=12 + - libglib >=2.80.0,<3.0a0 + - libstdcxx-ng >=12 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 355900 + timestamp: 1713896169874 +- pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl + name: black + version: 24.2.0 + sha256: e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6 + requires_dist: + - click>=8.0.0 + - mypy-extensions>=0.4.3 + - packaging>=22.0 + - pathspec>=0.9.0 + - platformdirs>=2 + - tomli>=1.1.0 ; python_full_version < '3.11' + - typing-extensions>=4.0.1 ; python_full_version < '3.11' + - colorama>=0.4.3 ; extra == 'colorama' + - aiohttp>=3.7.4,!=3.9.0 ; implementation_name == 'pypy' and sys_platform == 'win32' and extra == 'd' + - aiohttp>=3.7.4 ; (implementation_name != 'pypy' and extra == 'd') or (sys_platform != 'win32' and extra == 'd') + - ipython>=7.8.0 ; extra == 'jupyter' + - tokenize-rt>=3.2.0 ; extra == 'jupyter' + - uvloop>=0.15.2 ; extra == 'uvloop' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 + md5: 51a19bba1b8ebfb60df25cde030b7ebc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260341 + timestamp: 1757437258798 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda + sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 + md5: 74784ee3d225fc3dca89edb635b4e5cc + depends: + - __unix + license: ISC + purls: [] + size: 154402 + timestamp: 1754210968730 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7 + md5: 09262e66b19567aff4f592fb53b28760 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.5,<2.0a0 + - xorg-libx11 >=1.8.11,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + size: 978114 + timestamp: 1741554591855 +- pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl + name: certifi + version: 2025.8.3 + sha256: f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: cffi + version: 2.0.0 + sha256: c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + name: cfgv + version: 3.4.0 + sha256: b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.3 + sha256: 416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl + name: click + version: 8.2.1 + sha256: 61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl + name: codespell + version: 2.4.1 + sha256: 3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425 + requires_dist: + - build ; extra == 'dev' + - chardet ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-dependency ; extra == 'dev' + - pygments ; extra == 'dev' + - ruff ; extra == 'dev' + - tomli ; extra == 'dev' + - twine ; extra == 'dev' + - chardet ; extra == 'hard-encoding-detection' + - tomli ; python_full_version < '3.11' and extra == 'toml' + - chardet>=5.1.0 ; extra == 'types' + - mypy ; extra == 'types' + - pytest ; extra == 'types' + - pytest-cov ; extra == 'types' + - pytest-dependency ; extra == 'types' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: contourpy + version: 1.3.3 + sha256: 4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl + name: coverage + version: 7.10.6 + sha256: 0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + name: cycler + version: 0.12.1 + sha256: 85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 + requires_dist: + - ipython ; extra == 'docs' + - matplotlib ; extra == 'docs' + - numpydoc ; extra == 'docs' + - sphinx ; extra == 'docs' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + requires_python: '>=3.8' +- pypi: ./ + name: datajoint + version: 0.14.6 + sha256: 649c71b2cbfb0b38be5fe9a421035a7001e815777208d50274a7a31986c40e91 + requires_dist: + - numpy + - pymysql>=0.7.2 + - deepdiff + - pyparsing + - ipython + - pandas + - tqdm + - networkx + - pydot + - minio>=7.0.0 + - matplotlib + - faker + - urllib3 + - setuptools + - pre-commit ; extra == 'dev' + - black==24.2.0 ; extra == 'dev' + - flake8 ; extra == 'dev' + - isort ; extra == 'dev' + - codespell ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + requires_python: '>=3.9,<4.0' + editable: true +- conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068 + md5: 679616eb5ad4e521c83da4650860aba7 + depends: + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libexpat >=2.7.0,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - libglib >=2.84.2,<3.0a0 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 437860 + timestamp: 1747855126005 +- pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + name: decorator + version: 5.2.1 + sha256: d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + name: deepdiff + version: 8.6.1 + sha256: ee8708a7f7d37fb273a541fa24ad010ed484192cd0c4ffc0fa0ed5e2d4b9e78b + requires_dist: + - orderly-set>=5.4.1,<6 + - click~=8.1.0 ; extra == 'cli' + - pyyaml~=6.0.0 ; extra == 'cli' + - coverage~=7.6.0 ; extra == 'coverage' + - bump2version~=1.0.0 ; extra == 'dev' + - jsonpickle~=4.0.0 ; extra == 'dev' + - ipdb~=0.13.0 ; extra == 'dev' + - numpy~=2.2.0 ; python_full_version >= '3.10' and extra == 'dev' + - numpy~=2.0 ; python_full_version < '3.10' and extra == 'dev' + - python-dateutil~=2.9.0 ; extra == 'dev' + - orjson~=3.10.0 ; extra == 'dev' + - tomli~=2.2.0 ; extra == 'dev' + - tomli-w~=1.2.0 ; extra == 'dev' + - pandas~=2.2.0 ; extra == 'dev' + - polars~=1.21.0 ; extra == 'dev' + - nox==2025.5.1 ; extra == 'dev' + - uuid6==2025.0.1 ; extra == 'dev' + - sphinx~=6.2.0 ; extra == 'docs' + - sphinx-sitemap~=2.6.0 ; extra == 'docs' + - sphinxemoji~=0.3.0 ; extra == 'docs' + - orjson ; extra == 'optimize' + - flake8~=7.1.0 ; extra == 'static' + - flake8-pyproject~=1.2.3 ; extra == 'static' + - pydantic~=2.10.0 ; extra == 'static' + - pytest~=8.3.0 ; extra == 'test' + - pytest-benchmark~=5.1.0 ; extra == 'test' + - pytest-cov~=6.0.0 ; extra == 'test' + - python-dotenv~=1.0.0 ; extra == 'test' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + name: distlib + version: 0.4.0 + sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + name: docker + version: 7.1.0 + sha256: c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0 + requires_dist: + - pywin32>=304 ; sys_platform == 'win32' + - requests>=2.26.0 + - urllib3>=1.26.0 + - coverage==7.2.7 ; extra == 'dev' + - pytest-cov==4.1.0 ; extra == 'dev' + - pytest-timeout==2.1.0 ; extra == 'dev' + - pytest==7.4.2 ; extra == 'dev' + - ruff==0.1.8 ; extra == 'dev' + - myst-parser==0.18.0 ; extra == 'docs' + - sphinx==5.1.1 ; extra == 'docs' + - paramiko>=2.4.3 ; extra == 'ssh' + - websocket-client>=1.3.0 ; extra == 'websockets' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + sha256: 1e58ee2ed0f4699be202f23d49b9644b499836230da7dd5b2f63e6766acff89e + md5: a089d06164afd2d511347d3f87214e0b + depends: + - libgcc-ng >=10.3.0 + license: MIT + license_family: MIT + purls: [] + size: 1440699 + timestamp: 1648505042260 +- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + name: executing + version: 2.2.1 + sha256: 760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 + requires_dist: + - asttokens>=2.1.0 ; extra == 'tests' + - ipython ; extra == 'tests' + - pytest ; extra == 'tests' + - coverage ; extra == 'tests' + - coverage-enable-subprocess ; extra == 'tests' + - littleutils ; extra == 'tests' + - rich ; python_full_version >= '3.11' and extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl + name: faker + version: 37.8.0 + sha256: b08233118824423b5fc239f7dd51f145e7018082b4164f8da6a9994e1f1ae793 + requires_dist: + - tzdata + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl + name: filelock + version: 3.19.1 + sha256: d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl + name: flake8 + version: 7.3.0 + sha256: b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e + requires_dist: + - mccabe>=0.7.0,<0.8.0 + - pycodestyle>=2.14.0,<2.15.0 + - pyflakes>=3.4.0,<3.5.0 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b + md5: 0c96522c6bdaed4b1566d11387caaf45 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 397370 + timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c + md5: 34893075a5c9e55cdafac56607368fc6 + license: OFL-1.1 + license_family: Other + purls: [] + size: 96530 + timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 + md5: 4d59c254e01d9cde7957100457e2d5fb + license: OFL-1.1 + license_family: Other + purls: [] + size: 700814 + timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 + md5: 49023d73832ef61042f6a237cb2687e7 + license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 + license_family: Other + purls: [] + size: 1620504 + timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + sha256: 7093aa19d6df5ccb6ca50329ef8510c6acb6b0d8001191909397368b65b02113 + md5: 8f5b0b297b59e1ac160ad4beec99dbee + depends: + - __glibc >=2.17,<3.0.a0 + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 265599 + timestamp: 1730283881107 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 + md5: fee5683a3f04bd15cbd8318b096a27ab + depends: + - fonts-conda-forge + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3667 + timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + sha256: 53f23a3319466053818540bcdf2091f253cbdbab1e0e9ae7b9e509dcaa2a5e38 + md5: f766549260d6815b0c52253f1fb1bb29 + depends: + - font-ttf-dejavu-sans-mono + - font-ttf-inconsolata + - font-ttf-source-code-pro + - font-ttf-ubuntu + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4102 + timestamp: 1566932280397 +- pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl + name: fonttools + version: 4.59.2 + sha256: 6235fc06bcbdb40186f483ba9d5d68f888ea68aa3c8dac347e05a7c54346fbc8 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.23.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.23.0 ; extra == 'all' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda + sha256: bf8e4dffe46f7d25dc06f31038cacb01672c47b9f45201f065b0f4d00ab0a83e + md5: 4afc585cd97ba8a23809406cd8a9eda8 + depends: + - libfreetype 2.14.1 ha770c72_0 + - libfreetype6 2.14.1 h73754d4_0 + license: GPL-2.0-only OR FTL + purls: [] + size: 173114 + timestamp: 1757945422243 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda + sha256: 858283ff33d4c033f4971bf440cebff217d5552a5222ba994c49be990dacd40d + md5: f9f81ea472684d75b9dd8d0b328cf655 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-or-later + purls: [] + size: 61244 + timestamp: 1757438574066 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.1-h2b0a6b4_0.conda + sha256: b827285fe001806beeddcc30953d2bd07869aeb0efe4581d56432c92c06b0c48 + md5: 2935d9c0526277bd42373cf23d49d51f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libglib >=2.86.0,<3.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 579596 + timestamp: 1757867209855 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda + sha256: b77316bd5c8680bde4e5a7ab7013c8f0f10c1702cc6c3b0fd0fac3923a31fec3 + md5: 1a8e49615381c381659de1bc6a3bf9ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libglib 2.86.0 h1fed272_0 + license: LGPL-2.1-or-later + purls: [] + size: 117284 + timestamp: 1757403341964 +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda + sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c + md5: 2cd94587f3a401ae05e03a6caf09539d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 99596 + timestamp: 1755102025473 +- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + name: graphviz + version: '0.21' + sha256: 54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42 + requires_dist: + - build ; extra == 'dev' + - wheel ; extra == 'dev' + - twine ; extra == 'dev' + - flake8 ; extra == 'dev' + - flake8-pyproject ; extra == 'dev' + - pep8-naming ; extra == 'dev' + - tox>=3 ; extra == 'dev' + - pytest>=7,<8.1 ; extra == 'test' + - pytest-mock>=3 ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - sphinx>=5,<7 ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-rtd-theme>=0.2.5 ; extra == 'docs' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.2-h87b6fe6_0.conda + sha256: efbd7d483f3d79b7882515ccf229eceb7f4ff636ea2019044e98243722f428be + md5: 0adddc9b820f596638d8b0ff9e3b4823 + depends: + - __glibc >=2.17,<3.0.a0 + - adwaita-icon-theme + - cairo >=1.18.4,<2.0a0 + - fonts-conda-ecosystem + - gdk-pixbuf >=2.42.12,<3.0a0 + - gtk3 >=3.24.43,<4.0a0 + - gts >=0.7.6,<0.8.0a0 + - libexpat >=2.7.1,<3.0a0 + - libgcc >=14 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.84.3,<3.0a0 + - librsvg >=2.58.4,<3.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: EPL-1.0 + license_family: Other + purls: [] + size: 2427887 + timestamp: 1754732581595 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + sha256: d36263cbcbce34ec463ce92bd72efa198b55d987959eab6210cc256a0e79573b + md5: 67d00e9cfe751cfe581726c5eff7c184 + depends: + - __glibc >=2.17,<3.0.a0 + - at-spi2-atk >=2.38.0,<3.0a0 + - atk-1.0 >=2.38.0 + - cairo >=1.18.4,<2.0a0 + - epoxy >=1.5.10,<1.6.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - glib-tools + - harfbuzz >=11.0.0,<12.0a0 + - hicolor-icon-theme + - libcups >=2.3.3,<2.4.0a0 + - libcups >=2.3.3,<3.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.84.0,<3.0a0 + - liblzma >=5.6.4,<6.0a0 + - libxkbcommon >=1.8.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.3,<2.0a0 + - wayland >=1.23.1,<2.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxcomposite >=0.4.6,<1.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - xorg-libxdamage >=1.1.6,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxi >=1.8.2,<2.0a0 + - xorg-libxinerama >=1.1.5,<1.2.0a0 + - xorg-libxrandr >=1.5.4,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 5585389 + timestamp: 1743405684985 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + sha256: b5cd16262fefb836f69dc26d879b6508d29f8a5c5948a966c47fe99e2e19c99b + md5: 4d8df0b0db060d33c9a702ada998a8fe + depends: + - libgcc-ng >=12 + - libglib >=2.76.3,<3.0a0 + - libstdcxx-ng >=12 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 318312 + timestamp: 1686545244763 +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.0-h15599e2_0.conda + sha256: 04d33cef3345ce6e3fbbfb5539ebc8a3730026ea94ce6ace1f8f8d3551fa079c + md5: 47599428437d622bfee24fbd06a2d0b4 + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.0 + - libfreetype6 >=2.14.0 + - libgcc >=14 + - libglib >=2.86.0,<3.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + license: MIT + purls: [] + size: 2048134 + timestamp: 1757867460348 +- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + sha256: 336f29ceea9594f15cc8ec4c45fdc29e10796573c697ee0d57ebb7edd7e92043 + md5: bbf6f174dcd3254e19a2f5d2295ce808 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 13841 + timestamp: 1605162808667 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + md5: 8b189310083baabfb622af68fd9d3ae3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + purls: [] + size: 12129203 + timestamp: 1720853576813 +- pypi: https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl + name: identify + version: 2.6.14 + sha256: 11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e + requires_dist: + - ukkonen ; extra == 'license' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl + name: idna + version: '3.10' + sha256: 946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + - flake8>=7.1.1 ; extra == 'all' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl + name: iniconfig + version: 2.1.0 + sha256: 9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl + name: ipython + version: 9.5.0 + sha256: 88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72 + requires_dist: + - colorama ; sys_platform == 'win32' + - decorator + - ipython-pygments-lexers + - jedi>=0.16 + - matplotlib-inline + - pexpect>4.3 ; sys_platform != 'emscripten' and sys_platform != 'win32' + - prompt-toolkit>=3.0.41,<3.1.0 + - pygments>=2.4.0 + - stack-data + - traitlets>=5.13.0 + - typing-extensions>=4.6 ; python_full_version < '3.12' + - black ; extra == 'black' + - docrepr ; extra == 'doc' + - exceptiongroup ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - ipykernel ; extra == 'doc' + - ipython[test] ; extra == 'doc' + - matplotlib ; extra == 'doc' + - setuptools>=18.5 ; extra == 'doc' + - sphinx-toml==0.0.4 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx>=1.3 ; extra == 'doc' + - typing-extensions ; extra == 'doc' + - pytest ; extra == 'test' + - pytest-asyncio ; extra == 'test' + - testpath ; extra == 'test' + - packaging ; extra == 'test' + - ipython[test] ; extra == 'test-extra' + - curio ; extra == 'test-extra' + - jupyter-ai ; extra == 'test-extra' + - matplotlib!=3.2.0 ; extra == 'test-extra' + - nbformat ; extra == 'test-extra' + - nbclient ; extra == 'test-extra' + - ipykernel ; extra == 'test-extra' + - numpy>=1.23 ; extra == 'test-extra' + - pandas ; extra == 'test-extra' + - trio ; extra == 'test-extra' + - matplotlib ; extra == 'matplotlib' + - ipython[doc,matplotlib,test,test-extra] ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + name: ipython-pygments-lexers + version: 1.1.1 + sha256: a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c + requires_dist: + - pygments + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl + name: isort + version: 6.0.1 + sha256: 2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615 + requires_dist: + - colorama ; extra == 'colors' + - setuptools ; extra == 'plugins' + requires_python: '>=3.9.0' +- pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + name: jedi + version: 0.19.2 + sha256: a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 + requires_dist: + - parso>=0.8.4,<0.9.0 + - jinja2==2.11.3 ; extra == 'docs' + - markupsafe==1.1.1 ; extra == 'docs' + - pygments==2.8.1 ; extra == 'docs' + - alabaster==0.7.12 ; extra == 'docs' + - babel==2.9.1 ; extra == 'docs' + - chardet==4.0.0 ; extra == 'docs' + - commonmark==0.8.1 ; extra == 'docs' + - docutils==0.17.1 ; extra == 'docs' + - future==0.18.2 ; extra == 'docs' + - idna==2.10 ; extra == 'docs' + - imagesize==1.2.0 ; extra == 'docs' + - mock==1.0.1 ; extra == 'docs' + - packaging==20.9 ; extra == 'docs' + - pyparsing==2.4.7 ; extra == 'docs' + - pytz==2021.1 ; extra == 'docs' + - readthedocs-sphinx-ext==2.1.4 ; extra == 'docs' + - recommonmark==0.5.0 ; extra == 'docs' + - requests==2.25.1 ; extra == 'docs' + - six==1.15.0 ; extra == 'docs' + - snowballstemmer==2.1.0 ; extra == 'docs' + - sphinx-rtd-theme==0.4.3 ; extra == 'docs' + - sphinx==1.8.5 ; extra == 'docs' + - sphinxcontrib-serializinghtml==1.1.4 ; extra == 'docs' + - sphinxcontrib-websupport==1.2.4 ; extra == 'docs' + - urllib3==1.26.4 ; extra == 'docs' + - flake8==5.0.4 ; extra == 'qa' + - mypy==0.971 ; extra == 'qa' + - types-setuptools==67.2.0.1 ; extra == 'qa' + - django ; extra == 'testing' + - attrs ; extra == 'testing' + - colorama ; extra == 'testing' + - docopt ; extra == 'testing' + - pytest<9.0.0 ; extra == 'testing' + requires_python: '>=3.6' +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + size: 134088 + timestamp: 1754905959823 +- pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: kiwisolver + version: 1.4.9 + sha256: b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 + md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + depends: + - keyutils >=1.6.1,<2.0a0 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1370023 + timestamp: 1719463201255 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 + md5: 0be7c6e070c19105f966d3758448d018 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - binutils_impl_linux-64 2.44 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 676044 + timestamp: 1752032747103 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + sha256: 412381a43d5ff9bbed82cd52a0bbca5b90623f62e41007c9c42d3870c60945ff + md5: 9344155d33912347b37f0ae6c410a835 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 264243 + timestamp: 1745264221534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + sha256: cb83980c57e311783ee831832eb2c20ecb41e7dee6e86e8b70b8cef0e43eab55 + md5: d4a250da4737ee127fb1fa6452a9002e + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 4523621 + timestamp: 1749905341688 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + sha256: 8420748ea1cc5f18ecc5068b4f24c7a023cc9b20971c99c824ba10641fb95ddf + md5: 64f0c503da58ec25ebd359e4d990afa8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 72573 + timestamp: 1747040452262 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda + sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 + md5: 4211416ecba1866fab0c6470986c22d6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 74811 + timestamp: 1752719572741 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab + md5: ede4673863426c0883c0063d853bbd85 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 57433 + timestamp: 1743434498161 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda + sha256: 4641d37faeb97cf8a121efafd6afd040904d4bca8c46798122f417c31d5dfbec + md5: f4084e4e6577797150f9b04a4560ceb0 + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 7664 + timestamp: 1757945417134 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda + sha256: 4a7af818a3179fafb6c91111752954e29d3a2a950259c14a2fc7ba40a8b03652 + md5: 8e7251989bca326a28f4a5ffbd74557a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 386739 + timestamp: 1757945416744 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda + sha256: 0caed73aac3966bfbf5710e06c728a24c6c138605121a3dacb2e03440e8baa6a + md5: 264fbfba7fb20acf3b29cde153e345ce + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgomp 15.1.0 h767d61c_5 + - libgcc-ng ==15.1.0=*_5 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 824191 + timestamp: 1757042543820 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda + sha256: f54bb9c3be12b24be327f4c1afccc2969712e0b091cdfbd1d763fb3e61cda03f + md5: 069afdf8ea72504e48d23ae1171d951c + depends: + - libgcc 15.1.0 h767d61c_5 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29187 + timestamp: 1757042549554 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + sha256: 19e5be91445db119152217e8e8eec4fd0499d854acc7d8062044fb55a70971cd + md5: 68fc66282364981589ef36868b1a7c78 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.45,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GD + license_family: BSD + purls: [] + size: 177082 + timestamp: 1737548051015 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda + sha256: 33336bd55981be938f4823db74291e1323454491623de0be61ecbe6cf3a4619c + md5: b8e4c93f4ab70c3b6f6499299627dbdc + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.46,<10.47.0a0 + constrains: + - glib 2.86.0 *_0 + license: LGPL-2.1-or-later + purls: [] + size: 3978602 + timestamp: 1757403291664 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda + sha256: 125051d51a8c04694d0830f6343af78b556dd88cc249dfec5a97703ebfb1832d + md5: dcd5ff1940cd38f6df777cac86819d60 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 447215 + timestamp: 1757042483384 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-only + purls: [] + size: 790176 + timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + sha256: 98b399287e27768bf79d48faba8a99a2289748c65cd342ca21033fab1860d4a4 + md5: 9fa334557db9f63da6c9285fd2a48638 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 628947 + timestamp: 1745268527144 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda + sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee + md5: c7e925f37e3b40d893459e625f6a53f1 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 91183 + timestamp: 1748393666725 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda + sha256: e75a2723000ce3a4b9fd9b9b9ce77553556c93e475a4657db6ed01abc02ea347 + md5: 7af8e91b0deb5f8e25d1a595dea79614 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + purls: [] + size: 317390 + timestamp: 1753879899951 +- conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + sha256: a45ef03e6e700cc6ac6c375e27904531cf8ade27eb3857e080537ff283fb0507 + md5: d27665b20bc4d074b86e628b3ba5ab8b + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - freetype >=2.13.3,<3.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - harfbuzz >=11.0.0,<12.0a0 + - libgcc >=13 + - libglib >=2.84.0,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + constrains: + - __glibc >=2.17 + license: LGPL-2.1-or-later + purls: [] + size: 6543651 + timestamp: 1743368725313 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda + sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da + md5: 0b367fad34931cb79e0d6b7e5c06bb1c + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 932581 + timestamp: 1753948484112 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda + sha256: 0f5f61cab229b6043541c13538d75ce11bd96fb2db76f94ecf81997b1fde6408 + md5: 4e02a49aaa9d5190cb630fa43528fbe6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.1.0 h767d61c_5 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3896432 + timestamp: 1757042571458 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda + sha256: 7b8cabbf0ab4fe3581ca28fe8ca319f964078578a51dd2ca3f703c1d21ba23ff + md5: 8bba50c7f4679f08c861b597ad2bda6b + depends: + - libstdcxx 15.1.0 h8f9b012_5 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29233 + timestamp: 1757042603319 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda + sha256: c62694cd117548d810d2803da6d9063f78b1ffbf7367432c5388ce89474e9ebe + md5: b6093922931b535a7ba566b6f384fbe6 + depends: + - __glibc >=2.17,<3.0.a0 + - lerc >=4.0.0,<5.0a0 + - libdeflate >=1.24,<1.25.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 433078 + timestamp: 1755011934951 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda + sha256: 776e28735cee84b97e4d05dd5d67b95221a3e2c09b8b13e3d6dbe6494337d527 + md5: af930c65e9a79a3423d6d36e265cef65 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 37087 + timestamp: 1757334557450 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b + md5: aea31d2e5b1091feca96fcfe945c3cf9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 429011 + timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa + md5: 92ed62436b625154323d40d5f2f11dd7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + purls: [] + size: 395888 + timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda + sha256: 23f47e86cc1386e7f815fa9662ccedae151471862e971ea511c5c886aa723a54 + md5: 74e91c36d0eef3557915c68b6c2bef96 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - libxcb >=1.17.0,<2.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - xkeyboard-config + - xorg-libxau >=1.0.12,<2.0a0 + license: MIT/X11 Derivative + license_family: MIT + purls: [] + size: 791328 + timestamp: 1754703902365 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda + sha256: 03deb1ec6edfafc5aaeecadfc445ee436fecffcda11fcd97fde9b6632acb583f + md5: 10bcbd05e1c1c9d652fccb42b776a9fa + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 698448 + timestamp: 1754315344761 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: matplotlib + version: 3.10.6 + sha256: 84e82d9e0fd70c70bc55739defbd8055c54300750cbacf4740c9673a24d6933a + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=2.3.1 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl + name: matplotlib-inline + version: 0.1.7 + sha256: df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca + requires_dist: + - traitlets + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl + name: mccabe + version: 0.7.0 + sha256: 6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl + name: minio + version: 7.2.16 + sha256: 9288ab988ca57c181eb59a4c96187b293131418e28c164392186c2b89026b223 + requires_dist: + - argon2-cffi + - certifi + - pycryptodome + - typing-extensions + - urllib3 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + name: mypy-extensions + version: 1.1.0 + sha256: 1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + name: networkx + version: '3.5' + sha256: 0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec + requires_dist: + - numpy>=1.25 ; extra == 'default' + - scipy>=1.11.2 ; extra == 'default' + - matplotlib>=3.8 ; extra == 'default' + - pandas>=2.0 ; extra == 'default' + - pre-commit>=4.1 ; extra == 'developer' + - mypy>=1.15 ; extra == 'developer' + - sphinx>=8.0 ; extra == 'doc' + - pydata-sphinx-theme>=0.16 ; extra == 'doc' + - sphinx-gallery>=0.18 ; extra == 'doc' + - numpydoc>=1.8.0 ; extra == 'doc' + - pillow>=10 ; extra == 'doc' + - texext>=0.6.7 ; extra == 'doc' + - myst-nb>=1.1 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - osmnx>=2.0.0 ; extra == 'example' + - momepy>=0.7.2 ; extra == 'example' + - contextily>=1.6 ; extra == 'example' + - seaborn>=0.13 ; extra == 'example' + - cairocffi>=1.7 ; extra == 'example' + - igraph>=0.11 ; extra == 'example' + - scikit-learn>=1.5 ; extra == 'example' + - lxml>=4.6 ; extra == 'extra' + - pygraphviz>=1.14 ; extra == 'extra' + - pydot>=3.0.1 ; extra == 'extra' + - sympy>=1.10 ; extra == 'extra' + - pytest>=7.2 ; extra == 'test' + - pytest-cov>=4.0 ; extra == 'test' + - pytest-xdist>=3.0 ; extra == 'test' + - pytest-mpl ; extra == 'test-extras' + - pytest-randomly ; extra == 'test-extras' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl + name: nodeenv + version: 1.9.1 + sha256: ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: numpy + version: 2.3.3 + sha256: 5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e + md5: ffffb341206dd0dab0c36053c048d621 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3128847 + timestamp: 1754465526100 +- pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + name: orderly-set + version: 5.5.0 + sha256: 46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7 + requires_dist: + - coverage~=7.6.0 ; extra == 'coverage' + - bump2version~=1.0.0 ; extra == 'dev' + - ipdb~=0.13.0 ; extra == 'dev' + - orjson ; extra == 'optimize' + - flake8~=7.1.0 ; extra == 'static' + - flake8-pyproject~=1.2.3 ; extra == 'static' + - pytest~=8.3.0 ; extra == 'test' + - pytest-benchmark~=5.1.0 ; extra == 'test' + - pytest-cov~=6.0.0 ; extra == 'test' + - python-dotenv~=1.0.0 ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + name: packaging + version: '25.0' + sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pandas + version: 2.3.2 + sha256: 4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + sha256: 3613774ad27e48503a3a6a9d72017087ea70f1426f6e5541dbdb59a3b626eaaf + md5: 79f71230c069a287efe3a8614069ddf1 + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=13 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 455420 + timestamp: 1751292466873 +- pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + name: parso + version: 0.8.5 + sha256: 646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887 + requires_dist: + - pytest ; extra == 'testing' + - docopt ; extra == 'testing' + - flake8==5.0.4 ; extra == 'qa' + - mypy==0.971 ; extra == 'qa' + - types-setuptools==67.2.0.1 ; extra == 'qa' + requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl + name: pathspec + version: 0.12.1 + sha256: a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda + sha256: 5c7380c8fd3ad5fc0f8039069a45586aa452cf165264bc5a437ad80397b32934 + md5: 7fa07cb0fb1b625a089ccc01218ee5b1 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 1209177 + timestamp: 1756742976157 +- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + name: pexpect + version: 4.9.0 + sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 + requires_dist: + - ptyprocess>=0.5 +- pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 11.3.0 + sha256: 13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - typing-extensions ; python_full_version < '3.10' and extra == 'typing' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda + sha256: 43d37bc9ca3b257c5dd7bf76a8426addbdec381f6786ff441dc90b1a49143b6a + md5: c01af13bdc553d1a8fbfff6e8db075f0 + depends: + - libgcc >=14 + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + purls: [] + size: 450960 + timestamp: 1754665235234 +- pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl + name: platformdirs + version: 4.4.0 + sha256: abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 + requires_dist: + - furo>=2024.8.6 ; extra == 'docs' + - proselint>=0.14 ; extra == 'docs' + - sphinx-autodoc-typehints>=3 ; extra == 'docs' + - sphinx>=8.1.3 ; extra == 'docs' + - appdirs==1.4.4 ; extra == 'test' + - covdefaults>=2.3 ; extra == 'test' + - pytest-cov>=6 ; extra == 'test' + - pytest-mock>=3.14 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - mypy>=1.14.1 ; extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + name: pluggy + version: 1.6.0 + sha256: e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + requires_dist: + - pre-commit ; extra == 'dev' + - tox ; extra == 'dev' + - pytest ; extra == 'testing' + - pytest-benchmark ; extra == 'testing' + - coverage ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl + name: pre-commit + version: 4.3.0 + sha256: 2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 + requires_dist: + - cfgv>=2.0.0 + - identify>=1.0.0 + - nodeenv>=0.11.1 + - pyyaml>=5.1 + - virtualenv>=20.10.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + name: prompt-toolkit + version: 3.0.52 + sha256: 9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 + requires_dist: + - wcwidth + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 + md5: b3c17d95b5a10c6e64a21fa17573e70e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 8252 + timestamp: 1726802366959 +- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + name: ptyprocess + version: 0.7.0 + sha256: 4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 +- pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + name: pure-eval + version: 0.2.3 + sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 + requires_dist: + - pytest ; extra == 'tests' +- pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + name: pycodestyle + version: 2.14.0 + sha256: dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + name: pycparser + version: '2.23' + sha256: e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pycryptodome + version: 3.23.0 + sha256: c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + name: pydot + version: 4.0.1 + sha256: 869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6 + requires_dist: + - pyparsing>=3.1.0 + - ruff ; extra == 'lint' + - mypy ; extra == 'types' + - pydot[lint] ; extra == 'dev' + - pydot[types] ; extra == 'dev' + - chardet ; extra == 'dev' + - parameterized ; extra == 'dev' + - pydot[dev] ; extra == 'tests' + - tox ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-xdist[psutil] ; extra == 'tests' + - zest-releaser[recommended] ; extra == 'release' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl + name: pyflakes + version: 3.4.0 + sha256: f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + name: pygments + version: 2.19.2 + sha256: 86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + name: pymysql + version: 1.1.2 + sha256: e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9 + requires_dist: + - cryptography ; extra == 'rsa' + - pynacl>=1.4.0 ; extra == 'ed25519' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl + name: pyparsing + version: 3.2.4 + sha256: 91d0fcde680d42cd031daf3a6ba20da3107e08a75de50da58360e7d94ab24d36 + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl + name: pytest + version: 8.4.2 + sha256: 872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79 + requires_dist: + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1 + - packaging>=20 + - pluggy>=1.5,<2 + - pygments>=2.7.2 + - tomli>=1 ; python_full_version < '3.11' + - argcomplete ; extra == 'dev' + - attrs>=19.2 ; extra == 'dev' + - hypothesis>=3.56 ; extra == 'dev' + - mock ; extra == 'dev' + - requests ; extra == 'dev' + - setuptools ; extra == 'dev' + - xmlschema ; extra == 'dev' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + name: pytest-cov + version: 7.0.0 + sha256: 3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861 + requires_dist: + - coverage[toml]>=7.10.6 + - pluggy>=1.2 + - pytest>=7 + - process-tests ; extra == 'testing' + - pytest-xdist ; extra == 'testing' + - virtualenv ; extra == 'testing' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl + name: pytest-env + version: 1.1.5 + sha256: ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30 + requires_dist: + - pytest>=8.3.3 + - tomli>=2.0.1 ; python_full_version < '3.11' + - covdefaults>=2.3 ; extra == 'testing' + - coverage>=7.6.1 ; extra == 'testing' + - pytest-mock>=3.14 ; extra == 'testing' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda + build_number: 100 + sha256: 16cc30a5854f31ca6c3688337d34e37a79cdc518a06375fe3482ea8e2d6b34c8 + md5: 724dcf9960e933838247971da07fe5cf + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.2,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33583088 + timestamp: 1756911465277 + python_site_packages_path: lib/python3.13/site-packages +- pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + name: python-dateutil + version: 2.9.0.post0 + sha256: a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + requires_dist: + - six>=1.5 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + name: pytz + version: '2025.2' + sha256: 5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +- pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pyyaml + version: 6.0.2 + sha256: 70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 282480 + timestamp: 1740379431762 +- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + name: requests + version: 2.32.5 + sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.21.1,<3 + - certifi>=2017.4.17 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + name: setuptools + version: 80.9.0 + sha256: 062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 + requires_dist: + - pytest>=6,!=8.1.* ; extra == 'test' + - virtualenv>=13.0.0 ; extra == 'test' + - wheel>=0.44.0 ; extra == 'test' + - pip>=19.1 ; extra == 'test' + - packaging>=24.2 ; extra == 'test' + - jaraco-envs>=2.2 ; extra == 'test' + - pytest-xdist>=3 ; extra == 'test' + - jaraco-path>=3.7.2 ; extra == 'test' + - build[virtualenv]>=1.0.3 ; extra == 'test' + - filelock>=3.4.0 ; extra == 'test' + - ini2toml[lite]>=0.14 ; extra == 'test' + - tomli-w>=1.0.0 ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-perf ; sys_platform != 'cygwin' and extra == 'test' + - jaraco-develop>=7.21 ; python_full_version >= '3.9' and sys_platform != 'cygwin' and extra == 'test' + - pytest-home>=0.5 ; extra == 'test' + - pytest-subprocess ; extra == 'test' + - pyproject-hooks!=1.1 ; extra == 'test' + - jaraco-test>=5.5 ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pygments-github-lexers==0.0.5 ; extra == 'doc' + - sphinx-favicon ; extra == 'doc' + - sphinx-inline-tabs ; extra == 'doc' + - sphinx-reredirects ; extra == 'doc' + - sphinxcontrib-towncrier ; extra == 'doc' + - sphinx-notfound-page>=1,<2 ; extra == 'doc' + - pyproject-hooks!=1.1 ; extra == 'doc' + - towncrier<24.7 ; extra == 'doc' + - packaging>=24.2 ; extra == 'core' + - more-itertools>=8.8 ; extra == 'core' + - jaraco-text>=3.7 ; extra == 'core' + - importlib-metadata>=6 ; python_full_version < '3.10' and extra == 'core' + - tomli>=2.0.1 ; python_full_version < '3.11' and extra == 'core' + - wheel>=0.43.0 ; extra == 'core' + - platformdirs>=4.2.2 ; extra == 'core' + - jaraco-functools>=4 ; extra == 'core' + - more-itertools ; extra == 'core' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - ruff>=0.8.0 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=2.2 ; extra == 'enabler' + - pytest-mypy ; extra == 'type' + - mypy==1.14.* ; extra == 'type' + - importlib-metadata>=7.0.2 ; python_full_version < '3.10' and extra == 'type' + - jaraco-develop>=7.21 ; sys_platform != 'cygwin' and extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + name: six + version: 1.17.0 + sha256: 4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + name: stack-data + version: 0.6.3 + sha256: d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 + requires_dist: + - executing>=1.2.0 + - asttokens>=2.1.0 + - pure-eval + - pytest ; extra == 'tests' + - typeguard ; extra == 'tests' + - pygments ; extra == 'tests' + - littleutils ; extra == 'tests' + - cython ; extra == 'tests' +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 + md5: a0116df4f4ed05c303811a837d5b39d8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3285204 + timestamp: 1748387766691 +- pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + name: tqdm + version: 4.67.1 + sha256: 26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 + requires_dist: + - colorama ; sys_platform == 'win32' + - pytest>=6 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-asyncio>=0.24 ; extra == 'dev' + - nbval ; extra == 'dev' + - requests ; extra == 'discord' + - slack-sdk ; extra == 'slack' + - requests ; extra == 'telegram' + - ipywidgets>=6 ; extra == 'notebook' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + name: traitlets + version: 5.14.3 + sha256: b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f + requires_dist: + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx ; extra == 'docs' + - argcomplete>=3.0.3 ; extra == 'test' + - mypy>=1.7.0 ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-mypy-testing ; extra == 'test' + - pytest>=7.0,<8.2 ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + name: typing-extensions + version: 4.15.0 + sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + name: tzdata + version: '2025.2' + sha256: 1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8 + requires_python: '>=2' +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a + license: LicenseRef-Public-Domain + purls: [] + size: 122968 + timestamp: 1742727099393 +- pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + name: urllib3 + version: 2.5.0 + sha256: e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc + requires_dist: + - brotli>=1.0.9 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - zstandard>=0.18.0 ; extra == 'zstd' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl + name: virtualenv + version: 20.34.0 + sha256: 341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 + requires_dist: + - distlib>=0.3.7,<1 + - filelock>=3.12.2,<4 + - importlib-metadata>=6.6 ; python_full_version < '3.8' + - platformdirs>=3.9.1,<5 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - furo>=2023.7.26 ; extra == 'docs' + - proselint>=0.13 ; extra == 'docs' + - sphinx>=7.1.2,!=7.3 ; extra == 'docs' + - sphinx-argparse>=0.4 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' + - towncrier>=23.6 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'test' + - coverage-enable-subprocess>=1 ; extra == 'test' + - coverage>=7.2.7 ; extra == 'test' + - flaky>=3.7 ; extra == 'test' + - packaging>=23.1 ; extra == 'test' + - pytest-env>=0.8.2 ; extra == 'test' + - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') + - pytest-mock>=3.11.1 ; extra == 'test' + - pytest-randomly>=3.12 ; extra == 'test' + - pytest-timeout>=2.1 ; extra == 'test' + - pytest>=7.4 ; extra == 'test' + - setuptools>=68 ; extra == 'test' + - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + sha256: ba673427dcd480cfa9bbc262fd04a9b1ad2ed59a159bd8f7e750d4c52282f34c + md5: 0f2ca7906bf166247d1d760c3422cb8a + depends: + - __glibc >=2.17,<3.0.a0 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT + purls: [] + size: 330474 + timestamp: 1751817998141 +- pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl + name: wcwidth + version: 0.2.13 + sha256: 3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859 + requires_dist: + - backports-functools-lru-cache>=1.2.1 ; python_full_version < '3.2' +- conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + sha256: a5d4af601f71805ec67403406e147c48d6bad7aaeae92b0622b7e2396842d3fe + md5: 397a013c2dc5145a70737871aaa87e98 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 392406 + timestamp: 1749375847832 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b + md5: fb901ff28063514abb6046c9ec2c4a45 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 58628 + timestamp: 1734227592886 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 + md5: 1c74ff8c35dcadf952a16f752ca5aa49 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - xorg-libice >=1.1.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 27590 + timestamp: 1741896361728 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + sha256: 51909270b1a6c5474ed3978628b341b4d4472cd22610e5f22b506855a5e20f67 + md5: db038ce880f100acc74dba10302b5630 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libxcb >=1.17.0,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 835896 + timestamp: 1741901112627 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + sha256: ed10c9283974d311855ae08a16dfd7e56241fac632aec3b92e3cfe73cff31038 + md5: f6ebe2cb3f82ba6c057dde5d9debe4f7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 14780 + timestamp: 1734229004433 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + sha256: 753f73e990c33366a91fd42cc17a3d19bb9444b9ca5ff983605fa9e953baf57f + md5: d3c295b50f092ab525ffe3c2aa4b7413 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13603 + timestamp: 1727884600744 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a + md5: 2ccd714aa2242315acaf0a67faea780b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + purls: [] + size: 32533 + timestamp: 1730908305254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + sha256: 43b9772fd6582bf401846642c4635c47a9b0e36ca08116b3ec3df36ab96e0ec0 + md5: b5fcc7172d22516e1f965490e65e33a4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13217 + timestamp: 1727891438799 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + sha256: 6b250f3e59db07c2514057944a3ea2044d6a8cdde8a47b6497c254520fade1ee + md5: 8035c64cb77ed555e3f150b7b3972480 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 19901 + timestamp: 1727794976192 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + sha256: da5dc921c017c05f38a38bd75245017463104457b63a1ce633ed41f214159c14 + md5: febbab7d15033c913d53c7a2c102309d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 50060 + timestamp: 1727752228921 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + sha256: 2fef37e660985794617716eb915865ce157004a4d567ed35ec16514960ae9271 + md5: 4bdb303603e9821baf5fe5fdff1dc8f8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 19575 + timestamp: 1727794961233 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + sha256: 1a724b47d98d7880f26da40e45f01728e7638e6ec69f35a3e11f92acd05f9e7a + md5: 17dcc85db3c7886650b8908b183d6876 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 47179 + timestamp: 1727799254088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + sha256: 1b9141c027f9d84a9ee5eb642a0c19457c788182a5a73c5a9083860ac5c20a8c + md5: 5e2eb9bf77394fc2e5918beefec9f9ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13891 + timestamp: 1727908521531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + sha256: ac0f037e0791a620a69980914a77cb6bb40308e26db11698029d6708f5aa8e0d + md5: 2de7f99d6581a4a7adbff607b5c278ca + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + purls: [] + size: 29599 + timestamp: 1727794874300 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 + md5: 96d57aba173e878a2089d5638016dc5e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 33005 + timestamp: 1734229037766 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + sha256: 752fdaac5d58ed863bbf685bb6f98092fe1a488ea8ebb7ed7b606ccfce08637a + md5: 7bbe9a0cc0df0ac5f5a8ad6d6a11af2f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxi >=1.7.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 32808 + timestamp: 1727964811275 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb + md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 567578 + timestamp: 1742433379869 From de13442afe5a4be42ac4c5cc6faf78bf53b769ba Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 17:38:46 +0200 Subject: [PATCH 010/219] update .gitattributes for pixi --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..887a2c18f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true From 64fc0ac5d4064a5c3ced3acd6eff74e798a769b4 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 20:35:16 +0200 Subject: [PATCH 011/219] lint --- tests/conftest.py | 63 +++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c1bea5b78..2c16f1140 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,7 +29,6 @@ from . import schema, schema_adapted, schema_advanced, schema_external, schema_simple from . import schema_uuid as schema_uuid_module - # Configure logging for container management logger = logging.getLogger(__name__) @@ -46,8 +45,6 @@ def pytest_configure(config): pass - - # Global container registry for cleanup _active_containers = set() _docker_client = None @@ -64,18 +61,24 @@ def _get_docker_client(): def _cleanup_containers(): """Clean up any remaining containers""" if _active_containers: - logger.info(f"Emergency cleanup: {len(_active_containers)} containers to clean up") + logger.info( + f"Emergency cleanup: {len(_active_containers)} containers to clean up" + ) try: client = _get_docker_client() for container_id in list(_active_containers): try: container = client.containers.get(container_id) container.remove(force=True) - logger.info(f"Emergency cleanup: removed container {container_id[:12]}") + logger.info( + f"Emergency cleanup: removed container {container_id[:12]}" + ) except docker.errors.NotFound: logger.debug(f"Container {container_id[:12]} already removed") except Exception as e: - logger.error(f"Error cleaning up container {container_id[:12]}: {e}") + logger.error( + f"Error cleaning up container {container_id[:12]}: {e}" + ) finally: _active_containers.discard(container_id) except Exception as e: @@ -102,7 +105,9 @@ def _unregister_container(container): def _signal_handler(signum, frame): """Handle signals to ensure container cleanup""" - logger.warning(f"Received signal {signum}, performing emergency container cleanup...") + logger.warning( + f"Received signal {signum}, performing emergency container cleanup..." + ) _cleanup_containers() # Restore default signal handler and re-raise the signal @@ -115,6 +120,7 @@ def _signal_handler(signum, frame): # In pytest, we'll rely on fixture teardown and atexit handlers primarily try: import pytest + # If we're here, pytest is available, so only register SIGTERM (for CI/batch scenarios) signal.signal(signal.SIGTERM, _signal_handler) # Don't intercept SIGINT (Ctrl+C) to allow pytest's normal cancellation behavior @@ -150,9 +156,7 @@ def mysql_container(docker_client): container = docker_client.containers.run( f"datajoint/mysql:{mysql_ver}", name=container_name, - environment={ - "MYSQL_ROOT_PASSWORD": "password" - }, + environment={"MYSQL_ROOT_PASSWORD": "password"}, command="mysqld --default-authentication-plugin=mysql_native_password", ports={"3306/tcp": None}, # Let Docker assign random port detach=True, @@ -162,7 +166,7 @@ def mysql_container(docker_client): "timeout": 30000000000, # 30s in nanoseconds "retries": 5, "interval": 15000000000, # 15s in nanoseconds - } + }, ) # Register container for cleanup @@ -172,7 +176,9 @@ def mysql_container(docker_client): # Wait for health check max_wait = 120 # 2 minutes start_time = time.time() - logger.info(f"Waiting for MySQL container {container_name} to become healthy (max {max_wait}s)") + logger.info( + f"Waiting for MySQL container {container_name} to become healthy (max {max_wait}s)" + ) while time.time() - start_time < max_wait: container.reload() @@ -182,7 +188,9 @@ def mysql_container(docker_client): break time.sleep(2) else: - logger.error(f"MySQL container {container_name} failed to become healthy within {max_wait}s") + logger.error( + f"MySQL container {container_name} failed to become healthy within {max_wait}s" + ) container.remove(force=True) raise RuntimeError("MySQL container failed to become healthy") @@ -190,7 +198,9 @@ def mysql_container(docker_client): port_info = container.attrs["NetworkSettings"]["Ports"]["3306/tcp"] if port_info: host_port = port_info[0]["HostPort"] - logger.info(f"MySQL container {container_name} is healthy and accessible on localhost:{host_port}") + logger.info( + f"MySQL container {container_name} is healthy and accessible on localhost:{host_port}" + ) else: raise RuntimeError("Failed to get MySQL port mapping") @@ -223,14 +233,11 @@ def minio_container(docker_client): container = docker_client.containers.run( f"minio/minio:{minio_ver}", name=container_name, - environment={ - "MINIO_ACCESS_KEY": "datajoint", - "MINIO_SECRET_KEY": "datajoint" - }, - command=['server', '--address', ':9000', '/data'], + environment={"MINIO_ACCESS_KEY": "datajoint", "MINIO_SECRET_KEY": "datajoint"}, + command=["server", "--address", ":9000", "/data"], ports={"9000/tcp": None}, # Let Docker assign random port detach=True, - remove=True + remove=True, ) # Register container for cleanup @@ -250,20 +257,26 @@ def minio_container(docker_client): minio_url = f"http://localhost:{host_port}" max_wait = 60 start_time = time.time() - logger.info(f"Waiting for MinIO container {container_name} to become ready (max {max_wait}s)") + logger.info( + f"Waiting for MinIO container {container_name} to become ready (max {max_wait}s)" + ) while time.time() - start_time < max_wait: try: response = requests.get(f"{minio_url}/minio/health/live", timeout=5) if response.status_code == 200: - logger.info(f"MinIO container {container_name} is ready and accessible at {minio_url}") + logger.info( + f"MinIO container {container_name} is ready and accessible at {minio_url}" + ) break except requests.exceptions.RequestException: logger.debug(f"MinIO container {container_name} not ready yet, retrying...") pass time.sleep(2) else: - logger.error(f"MinIO container {container_name} failed to become ready within {max_wait}s") + logger.error( + f"MinIO container {container_name} failed to become ready within {max_wait}s" + ) container.remove(force=True) raise RuntimeError("MinIO container failed to become ready") @@ -336,7 +349,9 @@ def configure_datajoint_for_containers(mysql_container): os.environ["DJ_PORT"] = str(port) # Verify the environment variables were set - logger.info(f"🔧 Environment after setting: DJ_HOST={os.environ.get('DJ_HOST')}, DJ_PORT={os.environ.get('DJ_PORT')}") + logger.info( + f"🔧 Environment after setting: DJ_HOST={os.environ.get('DJ_HOST')}, DJ_PORT={os.environ.get('DJ_PORT')}" + ) # Also update DataJoint's configuration directly for in-process connections dj.config["database.host"] = host From b3f82d997d1f88fff2898bd8d1becd36e0473086 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 20:38:54 +0200 Subject: [PATCH 012/219] add astroid exemption to codespell rc --- .codespellrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codespellrc b/.codespellrc index a56ec23f4..fd5b26ffb 100644 --- a/.codespellrc +++ b/.codespellrc @@ -2,4 +2,4 @@ skip = .git,*.pdf,*.svg,*.csv,*.ipynb,*.drawio # Rever -- nobody knows # numer -- numerator variable -ignore-words-list = rever,numer +ignore-words-list = rever,numer,astroid From a321f9243be1234e5c64cdff88fa390c2f881797 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 20:54:42 +0200 Subject: [PATCH 013/219] spruce up linting workflow --- .codespellrc | 5 ----- .pre-commit-config.yaml | 41 +++++++++--------------------------- pyproject.toml | 46 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 51 insertions(+), 41 deletions(-) delete mode 100644 .codespellrc diff --git a/.codespellrc b/.codespellrc deleted file mode 100644 index a56ec23f4..000000000 --- a/.codespellrc +++ /dev/null @@ -1,5 +0,0 @@ -[codespell] -skip = .git,*.pdf,*.svg,*.csv,*.ipynb,*.drawio -# Rever -- nobody knows -# numer -- numerator variable -ignore-words-list = rever,numer diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a58e0483..6c38487d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,38 +20,17 @@ repos: rev: v2.4.1 hooks: - id: codespell -- repo: https://github.com/pycqa/isort - rev: 6.0.1 # Use the latest stable version + args: [--toml, pyproject.toml] +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 hooks: - - id: isort - args: - - --profile=black # Optional, makes isort compatible with Black -- repo: https://github.com/psf/black - rev: 25.1.0 # matching versions in pyproject.toml and github actions - hooks: - - id: black - args: ["--check", "-v", "datajoint", "tests", "--diff"] # --required-version is conflicting with pre-commit -- repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - # syntax tests - - id: flake8 - args: - - --select=E9,F63,F7,F82 - - --count - - --show-source - - --statistics - files: datajoint # a lot of files in tests are not compliant - # style tests - - id: flake8 - args: - - --ignore=E203,E722,W503 - - --count - - --max-complexity=62 - - --max-line-length=127 - - --statistics - - --per-file-ignores=datajoint/diagram.py:C901 - files: datajoint # a lot of files in tests are not compliant + # Run the linter + - id: ruff + args: [--fix] + files: ^(src/|tests/) + # Run the formatter + - id: ruff-format + files: ^(src/|tests/) - repo: https://github.com/rhysd/actionlint rev: v1.7.7 hooks: diff --git a/pyproject.toml b/pyproject.toml index b1d672af8..0aa805105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,17 +85,46 @@ test = [ ] dev = [ "pre-commit", - "black==24.2.0", - "flake8", - "isort", + "ruff", "codespell", # including test "pytest", "pytest-cov", ] -[tool.isort] -profile = "black" +[tool.ruff] +# Equivalent to flake8 configuration +line-length = 127 +target-version = "py39" + +[tool.ruff.lint] +# Enable specific rule sets equivalent to flake8 configuration +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "C90", # mccabe complexity +] + +# Ignore specific rules (equivalent to flake8 --ignore) +ignore = [ + "E203", # whitespace before ':' + "E722", # bare except +] + +# Per-file ignores (equivalent to flake8 --per-file-ignores) +[tool.ruff.lint.per-file-ignores] +"datajoint/diagram.py" = ["C901"] # function too complex + +[tool.ruff.lint.mccabe] +# Maximum complexity (equivalent to flake8 --max-complexity) +max-complexity = 62 + +[tool.ruff.format] +# Use black-compatible formatting +quote-style = "double" +indent-style = "space" +line-ending = "auto" [tool.setuptools] packages = ["datajoint"] @@ -103,3 +132,10 @@ package-dir = {"" = "src"} [tool.setuptools.dynamic] version = { attr = "datajoint.version.__version__"} + +[tool.codespell] +skip = ".git,*.pdf,*.svg,*.csv,*.ipynb,*.drawio" +# Rever -- nobody knows +# numer -- numerator variable +# astroid -- Python library name (not "asteroid") +ignore-words-list = "rever,numer,astroid" From 1aa30f49ec2b87c58d5f2d243d6572efd3f60a9d Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 20:55:24 +0200 Subject: [PATCH 014/219] lint with ruff --- src/datajoint/admin.py | 29 +-- src/datajoint/attribute_adapter.py | 10 +- src/datajoint/autopopulate.py | 93 ++------- src/datajoint/blob.py | 143 +++----------- src/datajoint/cli.py | 4 +- src/datajoint/condition.py | 68 ++----- src/datajoint/connection.py | 68 ++----- src/datajoint/declare.py | 134 ++++--------- src/datajoint/dependencies.py | 20 +- src/datajoint/diagram.py | 103 +++------- src/datajoint/expression.py | 242 ++++++------------------ src/datajoint/external.py | 98 +++------- src/datajoint/fetch.py | 84 ++------- src/datajoint/heading.py | 194 ++++++------------- src/datajoint/jobs.py | 15 +- src/datajoint/preview.py | 37 +--- src/datajoint/s3.py | 8 +- src/datajoint/schemas.py | 127 +++---------- src/datajoint/settings.py | 30 +-- src/datajoint/table.py | 291 +++++++---------------------- src/datajoint/user_tables.py | 43 +---- src/datajoint/utils.py | 8 +- tests/conftest.py | 52 ++---- tests/schema.py | 17 +- tests/schema_adapted.py | 1 - tests/schema_external.py | 1 - tests/schema_simple.py | 15 +- tests/schema_uuid.py | 8 +- tests/test_adapted_attributes.py | 12 +- tests/test_admin.py | 5 +- tests/test_aggr_regressions.py | 4 +- tests/test_alter.py | 29 +-- tests/test_attach.py | 9 +- tests/test_autopopulate.py | 3 - tests/test_blob.py | 24 +-- tests/test_blob_matlab.py | 20 +- tests/test_cascading_delete.py | 29 +-- tests/test_cli.py | 1 - tests/test_connection.py | 16 +- tests/test_declare.py | 64 ++----- tests/test_erd.py | 14 +- tests/test_external.py | 40 +--- tests/test_external_class.py | 9 +- tests/test_fetch.py | 16 +- tests/test_filepath.py | 16 +- tests/test_foreign_keys.py | 4 +- tests/test_jobs.py | 37 +--- tests/test_json.py | 65 +++---- tests/test_log.py | 4 +- tests/test_nan.py | 12 +- tests/test_privileges.py | 22 +-- tests/test_relation.py | 8 +- tests/test_relation_u.py | 5 +- tests/test_relational_operand.py | 162 +++++----------- tests/test_s3.py | 4 +- tests/test_schema.py | 24 +-- tests/test_settings.py | 11 +- tests/test_tls.py | 12 +- tests/test_university.py | 44 ++--- tests/test_update1.py | 21 +-- 60 files changed, 641 insertions(+), 2048 deletions(-) diff --git a/src/datajoint/admin.py b/src/datajoint/admin.py index e1eb803ec..c5e93f88f 100644 --- a/src/datajoint/admin.py +++ b/src/datajoint/admin.py @@ -20,18 +20,14 @@ def set_password(new_password=None, connection=None, update_config=None): logger.warning("Failed to confirm the password! Aborting password change.") return - if version.parse( - connection.query("select @@version;").fetchone()[0] - ) >= version.parse("5.7"): + if version.parse(connection.query("select @@version;").fetchone()[0]) >= version.parse("5.7"): # SET PASSWORD is deprecated as of MySQL 5.7 and removed in 8+ connection.query("ALTER USER user() IDENTIFIED BY '%s';" % new_password) else: connection.query("SET PASSWORD = PASSWORD('%s')" % new_password) logger.info("Password updated.") - if update_config or ( - update_config is None and user_choice("Update local setting?") == "yes" - ): + if update_config or (update_config is None and user_choice("Update local setting?") == "yes"): config["database.password"] = new_password config.save_local(verbose=True) @@ -67,17 +63,10 @@ def kill(restriction=None, connection=None, order_by=None): while True: print(" ID USER HOST STATE TIME INFO") print("+--+ +----------+ +-----------+ +-----------+ +-----+") - cur = ( - {k.lower(): v for k, v in elem.items()} - for elem in connection.query(query, as_dict=True) - ) + cur = ({k.lower(): v for k, v in elem.items()} for elem in connection.query(query, as_dict=True)) for process in cur: try: - print( - "{id:>4d} {user:<12s} {host:<12s} {state:<12s} {time:>7d} {info}".format( - **process - ) - ) + print("{id:>4d} {user:<12s} {host:<12s} {state:<12s} {time:>7d} {info}".format(**process)) except TypeError: print(process) response = input('process to kill or "q" to quit > ') @@ -111,15 +100,11 @@ def kill_quick(restriction=None, connection=None): if connection is None: connection = conn() - query = ( - "SELECT * FROM information_schema.processlist WHERE id <> CONNECTION_ID()" - + ("" if restriction is None else " AND (%s)" % restriction) + query = "SELECT * FROM information_schema.processlist WHERE id <> CONNECTION_ID()" + ( + "" if restriction is None else " AND (%s)" % restriction ) - cur = ( - {k.lower(): v for k, v in elem.items()} - for elem in connection.query(query, as_dict=True) - ) + cur = ({k.lower(): v for k, v in elem.items()} for elem in connection.query(query, as_dict=True)) nkill = 0 for process in cur: connection.query("kill %d" % process["id"]) diff --git a/src/datajoint/attribute_adapter.py b/src/datajoint/attribute_adapter.py index 2a8e59a51..12a34f27e 100644 --- a/src/datajoint/attribute_adapter.py +++ b/src/datajoint/attribute_adapter.py @@ -45,20 +45,14 @@ def get_adapter(context, adapter_name): try: adapter = context[adapter_name] except KeyError: - raise DataJointError( - "Attribute adapter '{adapter_name}' is not defined.".format( - adapter_name=adapter_name - ) - ) + raise DataJointError("Attribute adapter '{adapter_name}' is not defined.".format(adapter_name=adapter_name)) if not isinstance(adapter, AttributeAdapter): raise DataJointError( "Attribute adapter '{adapter_name}' must be an instance of datajoint.AttributeAdapter".format( adapter_name=adapter_name ) ) - if not isinstance(adapter.attribute_type, str) or not re.match( - r"^\w", adapter.attribute_type - ): + if not isinstance(adapter.attribute_type, str) or not re.match(r"^\w", adapter.attribute_type): raise DataJointError( "Invalid attribute type {type} in attribute adapter '{adapter_name}'".format( type=adapter.attribute_type, adapter_name=adapter_name diff --git a/src/datajoint/autopopulate.py b/src/datajoint/autopopulate.py index 226e64dda..53e64beeb 100644 --- a/src/datajoint/autopopulate.py +++ b/src/datajoint/autopopulate.py @@ -68,26 +68,15 @@ def key_source(self): def _rename_attributes(table, props): return ( - table.proj( - **{ - attr: ref - for attr, ref in props["attr_map"].items() - if attr != ref - } - ) + table.proj(**{attr: ref for attr, ref in props["attr_map"].items() if attr != ref}) if props["aliased"] else table.proj() ) if self._key_source is None: - parents = self.target.parents( - primary=True, as_objects=True, foreign_key_info=True - ) + parents = self.target.parents(primary=True, as_objects=True, foreign_key_info=True) if not parents: - raise DataJointError( - "A table must have dependencies " - "from its primary key for auto-populate to work" - ) + raise DataJointError("A table must have dependencies " "from its primary key for auto-populate to work") self._key_source = _rename_attributes(*parents[0]) for q in parents[1:]: self._key_source *= _rename_attributes(*q) @@ -139,11 +128,7 @@ def make(self, key): :raises NotImplementedError: If the derived class does not implement the required methods. """ - if not ( - hasattr(self, "make_fetch") - and hasattr(self, "make_insert") - and hasattr(self, "make_compute") - ): + if not (hasattr(self, "make_fetch") and hasattr(self, "make_insert") and hasattr(self, "make_compute")): # user must implement `make` raise NotImplementedError( "Subclasses of AutoPopulate must implement the method `make` " @@ -189,8 +174,7 @@ def _jobs_to_do(self, restrictions): """ if self.restriction: raise DataJointError( - "Cannot call populate on a restricted table. " - "Instead, pass conditions to populate() as arguments." + "Cannot call populate on a restricted table. " "Instead, pass conditions to populate() as arguments." ) todo = self.key_source @@ -206,11 +190,7 @@ def _jobs_to_do(self, restrictions): raise DataJointError( "The populate target lacks attribute %s " "from the primary key of key_source" - % next( - name - for name in todo.heading.primary_key - if name not in self.target.heading - ) + % next(name for name in todo.heading.primary_key if name not in self.target.heading) ) except StopIteration: pass @@ -259,12 +239,8 @@ def populate( valid_order = ["original", "reverse", "random"] if order not in valid_order: - raise DataJointError( - "The order argument must be one of %s" % str(valid_order) - ) - jobs = ( - self.connection.schemas[self.target.database].jobs if reserve_jobs else None - ) + raise DataJointError("The order argument must be one of %s" % str(valid_order)) + jobs = self.connection.schemas[self.target.database].jobs if reserve_jobs else None if reserve_jobs: # Define a signal handler for SIGTERM @@ -275,16 +251,12 @@ def handler(signum, frame): old_handler = signal.signal(signal.SIGTERM, handler) if keys is None: - keys = (self._jobs_to_do(restrictions) - self.target).fetch( - "KEY", limit=limit - ) + keys = (self._jobs_to_do(restrictions) - self.target).fetch("KEY", limit=limit) # exclude "error", "ignore" or "reserved" jobs if reserve_jobs: exclude_key_hashes = ( - jobs - & {"table_name": self.target.table_name} - & 'status in ("error", "ignore", "reserved")' + jobs & {"table_name": self.target.table_name} & 'status in ("error", "ignore", "reserved")' ).fetch("key_hash") keys = [key for key in keys if key_hash(key) not in exclude_key_hashes] @@ -311,11 +283,7 @@ def handler(signum, frame): ) if processes == 1: - for key in ( - tqdm(keys, desc=self.__class__.__name__) - if display_progress - else keys - ): + for key in tqdm(keys, desc=self.__class__.__name__) if display_progress else keys: status = self._populate1(key, jobs, **populate_kwargs) if status is True: success_list.append(1) @@ -328,14 +296,8 @@ def handler(signum, frame): self.connection.close() # disconnect parent process from MySQL server del self.connection._conn.ctx # SSLContext is not pickleable with ( - mp.Pool( - processes, _initialize_populate, (self, jobs, populate_kwargs) - ) as pool, - ( - tqdm(desc="Processes: ", total=nkeys) - if display_progress - else contextlib.nullcontext() - ) as progress_bar, + mp.Pool(processes, _initialize_populate, (self, jobs, populate_kwargs)) as pool, + tqdm(desc="Processes: ", total=nkeys) if display_progress else contextlib.nullcontext() as progress_bar, ): for status in pool.imap(_call_populate1, keys, chunksize=1): if status is True: @@ -357,9 +319,7 @@ def handler(signum, frame): "error_list": error_list, } - def _populate1( - self, key, jobs, suppress_errors, return_exception_objects, make_kwargs=None - ): + def _populate1(self, key, jobs, suppress_errors, return_exception_objects, make_kwargs=None): """ populates table for one source key, calling self.make inside a transaction. :param jobs: the jobs table or None if not reserve_jobs @@ -372,9 +332,7 @@ def _populate1( # use the legacy `_make_tuples` callback. make = self._make_tuples if hasattr(self, "_make_tuples") else self.make - if jobs is not None and not jobs.reserve( - self.target.table_name, self._job_key(key) - ): + if jobs is not None and not jobs.reserve(self.target.table_name, self._job_key(key)): return False # if make is a generator, it transaction can be delayed until the final stage @@ -399,23 +357,16 @@ def _populate1( # tripartite make - transaction is delayed until the final stage gen = make(dict(key), **(make_kwargs or {})) fetched_data = next(gen) - fetch_hash = deepdiff.DeepHash( - fetched_data, ignore_iterable_order=False - )[fetched_data] + fetch_hash = deepdiff.DeepHash(fetched_data, ignore_iterable_order=False)[fetched_data] computed_result = next(gen) # perform the computation # fetch and insert inside a transaction self.connection.start_transaction() gen = make(dict(key), **(make_kwargs or {})) # restart make fetched_data = next(gen) if ( - fetch_hash - != deepdiff.DeepHash(fetched_data, ignore_iterable_order=False)[ - fetched_data - ] + fetch_hash != deepdiff.DeepHash(fetched_data, ignore_iterable_order=False)[fetched_data] ): # raise error if fetched data has changed - raise DataJointError( - "Referential integrity failed! The `make_fetch` data has changed" - ) + raise DataJointError("Referential integrity failed! The `make_fetch` data has changed") gen.send(computed_result) # insert except (KeyboardInterrupt, SystemExit, Exception) as error: @@ -427,9 +378,7 @@ def _populate1( exception=error.__class__.__name__, msg=": " + str(error) if str(error) else "", ) - logger.debug( - f"Error making {key} -> {self.target.full_table_name} - {error_message}" - ) + logger.debug(f"Error making {key} -> {self.target.full_table_name} - {error_message}") if jobs is not None: # show error name and error message (if any) jobs.error( @@ -468,9 +417,7 @@ def progress(self, *restrictions, display=False): total - remaining, total, 100 - 100 * remaining / (total + 1e-12), - datetime.datetime.strftime( - datetime.datetime.now(), "%Y-%m-%d %H:%M:%S" - ), + datetime.datetime.strftime(datetime.datetime.now(), "%Y-%m-%d %H:%M:%S"), ), ) return remaining, total diff --git a/src/datajoint/blob.py b/src/datajoint/blob.py index 639789680..424d88779 100644 --- a/src/datajoint/blob.py +++ b/src/datajoint/blob.py @@ -113,9 +113,7 @@ def unpack(self, blob): self._blob = blob try: # decompress - prefix = next( - p for p in compression if self._blob[self._pos :].startswith(p) - ) + prefix = next(p for p in compression if self._blob[self._pos :].startswith(p)) except StopIteration: pass # assume uncompressed but could be unrecognized compression else: @@ -157,10 +155,7 @@ def read_blob(self, n_bytes=None): "u": self.read_uuid, # UUID }[data_structure_code] except KeyError: - raise DataJointError( - 'Unknown data structure code "%s". Upgrade datajoint.' - % data_structure_code - ) + raise DataJointError('Unknown data structure code "%s". Upgrade datajoint.' % data_structure_code) v = call() if n_bytes is not None and self._pos - start != n_bytes: raise DataJointError("Blob length check failed! Invalid blob") @@ -215,9 +210,7 @@ def pack_blob(self, obj): return self.pack_set(obj) if obj is None: return self.pack_none() - raise DataJointError( - "Packing object of type %s currently not supported!" % type(obj) - ) + raise DataJointError("Packing object of type %s currently not supported!" % type(obj)) def read_array(self): n_dims = int(self.read_value()) @@ -241,11 +234,7 @@ def read_array(self): data = data[::2].astype("U1") if n_dims == 2 and shape[0] == 1 or n_dims == 1: compact = data.squeeze() - data = ( - compact - if compact.shape == () - else np.array("".join(data.squeeze())) - ) + data = compact if compact.shape == () else np.array("".join(data.squeeze())) shape = (1,) else: data = self.read_value(dtype, count=n_elem) @@ -259,11 +248,7 @@ def pack_array(self, array): """ if "datetime64" in array.dtype.name: self.set_dj0() - blob = ( - b"A" - + np.uint64(array.ndim).tobytes() - + np.array(array.shape, dtype=np.uint64).tobytes() - ) + blob = b"A" + np.uint64(array.ndim).tobytes() + np.array(array.shape, dtype=np.uint64).tobytes() is_complex = np.iscomplexobj(array) if is_complex: array, imaginary = np.real(array), np.imag(array) @@ -277,19 +262,11 @@ def pack_array(self, array): raise DataJointError(f"Type {array.dtype} is ambiguous or unknown") blob += np.array([type_id, is_complex], dtype=np.uint32).tobytes() - if ( - array.dtype.char == "U" - or serialize_lookup[array.dtype]["scalar_type"] == "VOID" - ): - blob += b"".join( - len_u64(it) + it - for it in (self.pack_blob(e) for e in array.flatten(order="F")) - ) + if array.dtype.char == "U" or serialize_lookup[array.dtype]["scalar_type"] == "VOID": + blob += b"".join(len_u64(it) + it for it in (self.pack_blob(e) for e in array.flatten(order="F"))) self.set_dj0() # not supported by original mym elif serialize_lookup[array.dtype]["scalar_type"] == "CHAR": - blob += ( - array.view(np.uint8).astype(np.uint16).tobytes() - ) # convert to 16-bit chars for MATLAB + blob += array.view(np.uint8).astype(np.uint16).tobytes() # convert to 16-bit chars for MATLAB else: # numeric arrays if array.ndim == 0: # not supported by original mym self.set_dj0() @@ -323,34 +300,22 @@ def pack_recarray(self, array): + "\0".join(array.dtype.names).encode() # number of fields + b"\0" + b"".join( # field names - ( - self.pack_recarray(array[f]) - if array[f].dtype.fields - else self.pack_array(array[f]) - ) + (self.pack_recarray(array[f]) if array[f].dtype.fields else self.pack_array(array[f])) for f in array.dtype.names ) ) def read_sparse_array(self): - raise DataJointError( - "datajoint-python does not yet support sparse arrays. Issue (#590)" - ) + raise DataJointError("datajoint-python does not yet support sparse arrays. Issue (#590)") def read_int(self): - return int.from_bytes( - self.read_binary(self.read_value("uint16")), byteorder="little", signed=True - ) + return int.from_bytes(self.read_binary(self.read_value("uint16")), byteorder="little", signed=True) @staticmethod def pack_int(v): n_bytes = v.bit_length() // 8 + 1 assert 0 < n_bytes <= 0xFFFF, "Integers are limited to 65535 bytes" - return ( - b"\x0a" - + np.uint16(n_bytes).tobytes() - + v.to_bytes(n_bytes, byteorder="little", signed=True) - ) + return b"\x0a" + np.uint16(n_bytes).tobytes() + v.to_bytes(n_bytes, byteorder="little", signed=True) def read_bool(self): return bool(self.read_value("bool")) @@ -404,50 +369,32 @@ def pack_none(): return b"\xff" def read_tuple(self): - return tuple( - self.read_blob(self.read_value()) for _ in range(self.read_value()) - ) + return tuple(self.read_blob(self.read_value()) for _ in range(self.read_value())) def pack_tuple(self, t): - return ( - b"\1" - + len_u64(t) - + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t)) - ) + return b"\1" + len_u64(t) + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t)) def read_list(self): return list(self.read_blob(self.read_value()) for _ in range(self.read_value())) def pack_list(self, t): - return ( - b"\2" - + len_u64(t) - + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t)) - ) + return b"\2" + len_u64(t) + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t)) def read_set(self): return set(self.read_blob(self.read_value()) for _ in range(self.read_value())) def pack_set(self, t): - return ( - b"\3" - + len_u64(t) - + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t)) - ) + return b"\3" + len_u64(t) + b"".join(len_u64(it) + it for it in (self.pack_blob(i) for i in t)) def read_dict(self): - return dict( - (self.read_blob(self.read_value()), self.read_blob(self.read_value())) - for _ in range(self.read_value()) - ) + return dict((self.read_blob(self.read_value()), self.read_blob(self.read_value())) for _ in range(self.read_value())) def pack_dict(self, d): return ( b"\4" + len_u64(d) + b"".join( - b"".join((len_u64(it) + it) for it in packed) - for packed in (map(self.pack_blob, pair) for pair in d.items()) + b"".join((len_u64(it) + it) for it in packed) for packed in (map(self.pack_blob, pair) for pair in d.items()) ) ) @@ -460,16 +407,9 @@ def read_struct(self): if not n_fields: return np.array(None) # empty array field_names = [self.read_zero_terminated_string() for _ in range(n_fields)] - raw_data = [ - tuple( - self.read_blob(n_bytes=int(self.read_value())) for _ in range(n_fields) - ) - for __ in range(n_elem) - ] + raw_data = [tuple(self.read_blob(n_bytes=int(self.read_value())) for _ in range(n_fields)) for __ in range(n_elem)] data = np.array(raw_data, dtype=list(zip(field_names, repeat(object)))) - return self.squeeze( - data.reshape(shape, order="F"), convert_to_scalar=False - ).view(MatStruct) + return self.squeeze(data.reshape(shape, order="F"), convert_to_scalar=False).view(MatStruct) def pack_struct(self, array): """Serialize a Matlab struct array""" @@ -480,10 +420,7 @@ def pack_struct(self, array): + "\0".join(array.dtype.names).encode() # number of fields + b"\0" + b"".join( # field names - len_u64(it) + it - for it in ( - self.pack_blob(e) for rec in array.flatten(order="F") for e in rec - ) + len_u64(it) + it for it in (self.pack_blob(e) for rec in array.flatten(order="F") for e in rec) ) ) # values @@ -493,30 +430,19 @@ def read_cell_array(self): shape = self.read_value(count=n_dims) n_elem = int(np.prod(shape)) result = [self.read_blob(n_bytes=self.read_value()) for _ in range(n_elem)] - return ( - self.squeeze( - np.array(result).reshape(shape, order="F"), convert_to_scalar=False - ) - ).view(MatCell) + return (self.squeeze(np.array(result).reshape(shape, order="F"), convert_to_scalar=False)).view(MatCell) def pack_cell_array(self, array): return ( b"C" + np.array((array.ndim,) + array.shape, dtype=np.uint64).tobytes() - + b"".join( - len_u64(it) + it - for it in (self.pack_blob(e) for e in array.flatten(order="F")) - ) + + b"".join(len_u64(it) + it for it in (self.pack_blob(e) for e in array.flatten(order="F"))) ) def read_datetime(self): """deserialize datetime.date, .time, or .datetime""" date, time = self.read_value("int32"), self.read_value("int64") - date = ( - datetime.date(year=date // 10000, month=(date // 100) % 100, day=date % 100) - if date >= 0 - else None - ) + date = datetime.date(year=date // 10000, month=(date // 100) % 100, day=date % 100) if date >= 0 else None time = ( datetime.time( hour=(time // 10000000000) % 100, @@ -538,14 +464,9 @@ def pack_datetime(d): else: date, time = None, d return b"t" + ( - np.int32( - -1 if date is None else (date.year * 100 + date.month) * 100 + date.day - ).tobytes() + np.int32(-1 if date is None else (date.year * 100 + date.month) * 100 + date.day).tobytes() + np.int64( - -1 - if time is None - else ((time.hour * 100 + time.minute) * 100 + time.second) * 1000000 - + time.microsecond + -1 if time is None else ((time.hour * 100 + time.minute) * 100 + time.second) * 1000000 + time.microsecond ).tobytes() ) @@ -576,9 +497,7 @@ def read_binary(self, size): def pack(self, obj, compress): self.protocol = b"mYm\0" # will be replaced with dj0 if new features are used - blob = self.pack_blob( - obj - ) # this may reset the protocol and must precede protocol evaluation + blob = self.pack_blob(obj) # this may reset the protocol and must precede protocol evaluation blob = self.protocol + blob if compress and len(blob) > 1000: compressed = b"ZL123\0" + len_u64(blob) + zlib.compress(blob) @@ -590,9 +509,7 @@ def pack(self, obj, compress): def pack(obj, compress=True): if bypass_serialization: # provide a way to move blobs quickly without de/serialization - assert isinstance(obj, bytes) and obj.startswith( - (b"ZL123\0", b"mYm\0", b"dj0\0") - ) + assert isinstance(obj, bytes) and obj.startswith((b"ZL123\0", b"mYm\0", b"dj0\0")) return obj return Blob().pack(obj, compress=compress) @@ -600,9 +517,7 @@ def pack(obj, compress=True): def unpack(blob, squeeze=False): if bypass_serialization: # provide a way to move blobs quickly without de/serialization - assert isinstance(blob, bytes) and blob.startswith( - (b"ZL123\0", b"mYm\0", b"dj0\0") - ) + assert isinstance(blob, bytes) and blob.startswith((b"ZL123\0", b"mYm\0", b"dj0\0")) return blob if blob is not None: return Blob(squeeze=squeeze).unpack(blob) diff --git a/src/datajoint/cli.py b/src/datajoint/cli.py index 3b7e72c25..6437ebbc5 100644 --- a/src/datajoint/cli.py +++ b/src/datajoint/cli.py @@ -17,9 +17,7 @@ def cli(args: list = None): description="DataJoint console interface.", conflict_handler="resolve", ) - parser.add_argument( - "-V", "--version", action="version", version=f"{dj.__name__} {dj.__version__}" - ) + parser.add_argument("-V", "--version", action="version", version=f"{dj.__name__} {dj.__version__}") parser.add_argument( "-u", "--user", diff --git a/src/datajoint/condition.py b/src/datajoint/condition.py index 96cfbb6ef..f77cb2a2d 100644 --- a/src/datajoint/condition.py +++ b/src/datajoint/condition.py @@ -15,9 +15,7 @@ from .errors import DataJointError -JSON_PATTERN = re.compile( - r"^(?P\w+)(\.(?P[\w.*\[\]]+))?(:(?P[\w(,\s)]+))?$" -) +JSON_PATTERN = re.compile(r"^(?P\w+)(\.(?P[\w.*\[\]]+))?(:(?P[\w(,\s)]+))?$") def translate_attribute(key): @@ -29,10 +27,7 @@ def translate_attribute(key): return match, match["attr"] else: return match, "json_value(`{}`, _utf8mb4'$.{}'{})".format( - *[ - ((f" returning {v}" if k == "type" else v) if v else "") - for k, v in match.items() - ] + *[((f" returning {v}" if k == "type" else v) if v else "") for k, v in match.items()] ) @@ -115,21 +110,12 @@ def assert_join_compatibility(expr1, expr2): for rel in (expr1, expr2): if not isinstance(rel, (U, QueryExpression)): - raise DataJointError( - "Object %r is not a QueryExpression and cannot be joined." % rel - ) - if not isinstance(expr1, U) and not isinstance( - expr2, U - ): # dj.U is always compatible + raise DataJointError("Object %r is not a QueryExpression and cannot be joined." % rel) + if not isinstance(expr1, U) and not isinstance(expr2, U): # dj.U is always compatible try: raise DataJointError( "Cannot join query expressions on dependent attribute `%s`" - % next( - r - for r in set(expr1.heading.secondary_attributes).intersection( - expr2.heading.secondary_attributes - ) - ) + % next(r for r in set(expr1.heading.secondary_attributes).intersection(expr2.heading.secondary_attributes)) ) except StopIteration: pass # all ok @@ -152,11 +138,7 @@ def prep_value(k, v): key_match, k = translate_attribute(k) if key_match["path"] is None: k = f"`{k}`" - if ( - query_expression.heading[key_match["attr"]].json - and key_match["path"] is not None - and isinstance(v, dict) - ): + if query_expression.heading[key_match["attr"]].json and key_match["path"] is not None and isinstance(v, dict): return f"{k}='{json.dumps(v)}'" if v is None: return f"{k} IS NULL" @@ -165,9 +147,7 @@ def prep_value(k, v): try: v = uuid.UUID(v) except (AttributeError, ValueError): - raise DataJointError( - "Badly formed UUID {v} in restriction by `{k}`".format(k=k, v=v) - ) + raise DataJointError("Badly formed UUID {v} in restriction by `{k}`".format(k=k, v=v)) return f"{k}=X'{v.bytes.hex()}'" if isinstance( v, @@ -196,20 +176,12 @@ def combine_conditions(negate, conditions): # restrict by string if isinstance(condition, str): columns.update(extract_column_names(condition)) - return combine_conditions( - negate, conditions=[condition.strip().replace("%", "%%")] - ) # escape %, see issue #376 + return combine_conditions(negate, conditions=[condition.strip().replace("%", "%%")]) # escape %, see issue #376 # restrict by AndList if isinstance(condition, AndList): # omit all conditions that evaluate to True - items = [ - item - for item in ( - make_condition(query_expression, cond, columns) for cond in condition - ) - if item is not True - ] + items = [item for item in (make_condition(query_expression, cond, columns) for cond in condition) if item is not True] if any(item is False for item in items): return negate # if any item is False, the whole thing is False if not items: @@ -226,9 +198,7 @@ def combine_conditions(negate, conditions): # restrict by a mapping/dict -- convert to an AndList of string equality conditions if isinstance(condition, collections.abc.Mapping): - common_attributes = set(c.split(".", 1)[0] for c in condition).intersection( - query_expression.heading.names - ) + common_attributes = set(c.split(".", 1)[0] for c in condition).intersection(query_expression.heading.names) if not common_attributes: return not negate # no matching attributes -> evaluates to True columns.update(common_attributes) @@ -243,9 +213,7 @@ def combine_conditions(negate, conditions): # restrict by a numpy record -- convert to an AndList of string equality conditions if isinstance(condition, numpy.void): - common_attributes = set(condition.dtype.fields).intersection( - query_expression.heading.names - ) + common_attributes = set(condition.dtype.fields).intersection(query_expression.heading.names) if not common_attributes: return not negate # no matching attributes -> evaluate to True columns.update(common_attributes) @@ -267,9 +235,7 @@ def combine_conditions(negate, conditions): if isinstance(condition, QueryExpression): if check_compatibility: assert_join_compatibility(query_expression, condition) - common_attributes = [ - q for q in condition.heading.names if q in query_expression.heading.names - ] + common_attributes = [q for q in condition.heading.names if q in query_expression.heading.names] columns.update(common_attributes) if isinstance(condition, Aggregation): condition = condition.make_subquery() @@ -294,16 +260,10 @@ def combine_conditions(negate, conditions): except TypeError: raise DataJointError("Invalid restriction type %r" % condition) else: - or_list = [ - item for item in or_list if item is not False - ] # ignore False conditions + or_list = [item for item in or_list if item is not False] # ignore False conditions if any(item is True for item in or_list): # if any item is True, entirely True return not negate - return ( - f"{'NOT ' if negate else ''} ({' OR '.join(or_list)})" - if or_list - else negate - ) + return f"{'NOT ' if negate else ''} ({' OR '.join(or_list)})" if or_list else negate def extract_column_names(sql_expression): diff --git a/src/datajoint/connection.py b/src/datajoint/connection.py index 21b1c97a4..545595fed 100644 --- a/src/datajoint/connection.py +++ b/src/datajoint/connection.py @@ -40,9 +40,7 @@ def translate_query_error(client_error, query): # Loss of connection errors if err in (0, "(0, '')"): - return errors.LostConnectionError( - "Server connection lost due to an interface error.", *args - ) + return errors.LostConnectionError("Server connection lost due to an interface error.", *args) if err == 2006: return errors.LostConnectionError("Connection timed out", *args) if err == 2013: @@ -73,9 +71,7 @@ def translate_query_error(client_error, query): return client_error -def conn( - host=None, user=None, password=None, *, init_fun=None, reset=False, use_tls=None -): +def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use_tls=None): """ Returns a persistent connection object to be shared by multiple modules. If the connection is not yet established or reset=True, a new connection is set up. @@ -100,9 +96,7 @@ def conn( user = input("Please enter DataJoint username: ") if password is None: password = getpass(prompt="Please enter DataJoint password: ") - init_fun = ( - init_fun if init_fun is not None else config["connection.init_function"] - ) + init_fun = init_fun if init_fun is not None else config["connection.init_function"] use_tls = use_tls if use_tls is not None else config["database.use_tls"] conn.connection = Connection(host, user, password, None, init_fun, use_tls) return conn.connection @@ -156,25 +150,17 @@ def __init__(self, host, user, password, port=None, init_fun=None, use_tls=None) port = config["database.port"] self.conn_info = dict(host=host, port=port, user=user, passwd=password) if use_tls is not False: - self.conn_info["ssl"] = ( - use_tls if isinstance(use_tls, dict) else {"ssl": {}} - ) + self.conn_info["ssl"] = use_tls if isinstance(use_tls, dict) else {"ssl": {}} self.conn_info["ssl_input"] = use_tls self.init_fun = init_fun self._conn = None self._query_cache = None self.connect() if self.is_connected: - logger.info( - "DataJoint {version} connected to {user}@{host}:{port}".format( - version=__version__, **self.conn_info - ) - ) + logger.info("DataJoint {version} connected to {user}@{host}:{port}".format(version=__version__, **self.conn_info)) self.connection_id = self.query("SELECT connection_id()").fetchone()[0] else: - raise errors.LostConnectionError( - "Connection failed {user}@{host}:{port}".format(**self.conn_info) - ) + raise errors.LostConnectionError("Connection failed {user}@{host}:{port}".format(**self.conn_info)) self._in_transaction = False self.schemas = dict() self.dependencies = Dependencies(self) @@ -184,9 +170,7 @@ def __eq__(self, other): def __repr__(self): connected = "connected" if self.is_connected else "disconnected" - return "DataJoint connection ({connected}) {user}@{host}:{port}".format( - connected=connected, **self.conn_info - ) + return "DataJoint connection ({connected}) {user}@{host}:{port}".format(connected=connected, **self.conn_info) def connect(self): """Connect to the database server.""" @@ -198,11 +182,7 @@ def connect(self): sql_mode="NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO," "STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY", charset=config["connection.charset"], - **{ - k: v - for k, v in self.conn_info.items() - if k not in ["ssl_input"] - }, + **{k: v for k, v in self.conn_info.items() if k not in ["ssl_input"]}, ) except client.err.InternalError: self._conn = client.connect( @@ -213,11 +193,7 @@ def connect(self): **{ k: v for k, v in self.conn_info.items() - if not ( - k == "ssl_input" - or k == "ssl" - and self.conn_info["ssl_input"] is None - ) + if not (k == "ssl_input" or k == "ssl" and self.conn_info["ssl_input"] is None) }, ) self._conn.autocommit(True) @@ -235,10 +211,7 @@ def set_query_cache(self, query_cache=None): def purge_query_cache(self): """Purges all query cache.""" - if ( - isinstance(config.get(cache_key), str) - and pathlib.Path(config[cache_key]).is_dir() - ): + if isinstance(config.get(cache_key), str) and pathlib.Path(config[cache_key]).is_dir(): for path in pathlib.Path(config[cache_key]).iterdir(): if not path.is_dir(): path.unlink() @@ -274,9 +247,7 @@ def _execute_query(cursor, query, args, suppress_warnings): except client.err.Error as err: raise translate_query_error(err, query) - def query( - self, query, args=(), *, as_dict=False, suppress_warnings=True, reconnect=None - ): + def query(self, query, args=(), *, as_dict=False, suppress_warnings=True, reconnect=None): """ Execute the specified query and return the tuple generator (cursor). @@ -290,18 +261,11 @@ def query( # check cache first: use_query_cache = bool(self._query_cache) if use_query_cache and not re.match(r"\s*(SELECT|SHOW)", query): - raise errors.DataJointError( - "Only SELECT queries are allowed when query caching is on." - ) + raise errors.DataJointError("Only SELECT queries are allowed when query caching is on.") if use_query_cache: if not config[cache_key]: - raise errors.DataJointError( - f"Provide filepath dj.config['{cache_key}'] when using query caching." - ) - hash_ = uuid_from_buffer( - (str(self._query_cache) + re.sub(r"`\$\w+`", "", query)).encode() - + pack(args) - ) + raise errors.DataJointError(f"Provide filepath dj.config['{cache_key}'] when using query caching.") + hash_ = uuid_from_buffer((str(self._query_cache) + re.sub(r"`\$\w+`", "", query)).encode() + pack(args)) cache_path = pathlib.Path(config[cache_key]) / str(hash_) try: buffer = cache_path.read_bytes() @@ -324,9 +288,7 @@ def query( self.connect() if self._in_transaction: self.cancel_transaction() - raise errors.LostConnectionError( - "Connection was lost during a transaction." - ) + raise errors.LostConnectionError("Connection was lost during a transaction.") logger.debug("Re-executing") cursor = self._conn.cursor(cursor=cursor_class) self._execute_query(cursor, query, args, suppress_warnings) diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index 304476798..e706347c9 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -70,15 +70,9 @@ def match_type(attribute_type): try: - return next( - category - for category, pattern in TYPE_PATTERN.items() - if pattern.match(attribute_type) - ) + return next(category for category, pattern in TYPE_PATTERN.items() if pattern.match(attribute_type)) except StopIteration: - raise DataJointError( - "Unsupported attribute type {type}".format(type=attribute_type) - ) + raise DataJointError("Unsupported attribute type {type}".format(type=attribute_type)) logger = logging.getLogger(__name__.split(".")[0]) @@ -90,20 +84,14 @@ def build_foreign_key_parser_old(): left = pp.Literal("(").suppress() right = pp.Literal(")").suppress() attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]")) - new_attrs = pp.Optional( - left + pp.delimitedList(attribute_name) + right - ).setResultsName("new_attrs") + new_attrs = pp.Optional(left + pp.delimitedList(attribute_name) + right).setResultsName("new_attrs") arrow = pp.Literal("->").suppress() lbracket = pp.Literal("[").suppress() rbracket = pp.Literal("]").suppress() option = pp.Word(pp.srange("[a-zA-Z]")) - options = pp.Optional( - lbracket + pp.delimitedList(option) + rbracket - ).setResultsName("options") + options = pp.Optional(lbracket + pp.delimitedList(option) + rbracket).setResultsName("options") ref_table = pp.Word(pp.alphas, pp.alphanums + "._").setResultsName("ref_table") - ref_attrs = pp.Optional( - left + pp.delimitedList(attribute_name) + right - ).setResultsName("ref_attrs") + ref_attrs = pp.Optional(left + pp.delimitedList(attribute_name) + right).setResultsName("ref_attrs") return new_attrs + arrow + options + ref_table + ref_attrs @@ -112,9 +100,7 @@ def build_foreign_key_parser(): lbracket = pp.Literal("[").suppress() rbracket = pp.Literal("]").suppress() option = pp.Word(pp.srange("[a-zA-Z]")) - options = pp.Optional( - lbracket + pp.delimitedList(option) + rbracket - ).setResultsName("options") + options = pp.Optional(lbracket + pp.delimitedList(option) + rbracket).setResultsName("options") ref_table = pp.restOfLine.setResultsName("ref_table") return arrow + options + ref_table @@ -122,16 +108,12 @@ def build_foreign_key_parser(): def build_attribute_parser(): quoted = pp.QuotedString('"') ^ pp.QuotedString("'") colon = pp.Literal(":").suppress() - attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]")).setResultsName( - "name" - ) + attribute_name = pp.Word(pp.srange("[a-z]"), pp.srange("[a-z0-9_]")).setResultsName("name") data_type = ( pp.Combine(pp.Word(pp.alphas) + pp.SkipTo("#", ignore=quoted)) ^ pp.QuotedString("<", endQuoteChar=">", unquoteResults=False) ).setResultsName("type") - default = pp.Literal("=").suppress() + pp.SkipTo( - colon, ignore=quoted - ).setResultsName("default") + default = pp.Literal("=").suppress() + pp.SkipTo(colon, ignore=quoted).setResultsName("default") comment = pp.Literal("#").suppress() + pp.restOfLine.setResultsName("comment") return attribute_name + pp.Optional(default) + colon + data_type + comment @@ -151,9 +133,7 @@ def is_foreign_key(line): return arrow_position >= 0 and not any(c in line[:arrow_position] for c in "\"#'") -def compile_foreign_key( - line, context, attributes, primary_key, attr_sql, foreign_key_sql, index_sql -): +def compile_foreign_key(line, context, attributes, primary_key, attr_sql, foreign_key_sql, index_sql): """ :param line: a line from a table definition :param context: namespace containing referenced objects @@ -176,9 +156,7 @@ def compile_foreign_key( try: ref = eval(result.ref_table, context) except Exception: - raise DataJointError( - "Foreign key reference %s could not be resolved" % result.ref_table - ) + raise DataJointError("Foreign key reference %s could not be resolved" % result.ref_table) options = [opt.upper() for opt in result.options] for opt in options: # check for invalid options @@ -187,9 +165,7 @@ def compile_foreign_key( is_nullable = "NULLABLE" in options is_unique = "UNIQUE" in options if is_nullable and primary_key is not None: - raise DataJointError( - 'Primary dependencies cannot be nullable in line "{line}"'.format(line=line) - ) + raise DataJointError('Primary dependencies cannot be nullable in line "{line}"'.format(line=line)) if isinstance(ref, type) and issubclass(ref, Table): ref = ref() @@ -201,10 +177,7 @@ def compile_foreign_key( or len(ref.support) != 1 or not isinstance(ref.support[0], str) ): - raise DataJointError( - 'Dependency "%s" is not supported (yet). Use a base table or its projection.' - % result.ref_table - ) + raise DataJointError('Dependency "%s" is not supported (yet). Use a base table or its projection.' % result.ref_table) # declare new foreign key attributes for attr in ref.primary_key: @@ -212,9 +185,7 @@ def compile_foreign_key( attributes.append(attr) if primary_key is not None: primary_key.append(attr) - attr_sql.append( - ref.heading[attr].sql.replace("NOT NULL ", "", int(is_nullable)) - ) + attr_sql.append(ref.heading[attr].sql.replace("NOT NULL ", "", int(is_nullable))) # declare the foreign key foreign_key_sql.append( @@ -227,20 +198,14 @@ def compile_foreign_key( # declare unique index if is_unique: - index_sql.append( - "UNIQUE INDEX ({attrs})".format( - attrs=",".join("`%s`" % attr for attr in ref.primary_key) - ) - ) + index_sql.append("UNIQUE INDEX ({attrs})".format(attrs=",".join("`%s`" % attr for attr in ref.primary_key))) def prepare_declare(definition, context): # split definition into lines definition = re.split(r"\s*\n\s*", definition.strip()) # check for optional table comment - table_comment = ( - definition.pop(0)[1:].strip() if definition[0].startswith("#") else "" - ) + table_comment = definition.pop(0)[1:].strip() if definition[0].startswith("#") else "" if table_comment.startswith(":"): raise DataJointError('Table comment must not start with a colon ":"') in_key = True # parse primary keys @@ -315,15 +280,9 @@ def declare(full_table_name, definition, context): ) = prepare_declare(definition, context) if config.get("add_hidden_timestamp", False): - metadata_attr_sql = [ - "`_{full_table_name}_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP" - ] + metadata_attr_sql = ["`_{full_table_name}_timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP"] attribute_sql.extend( - attr.format( - full_table_name=sha1( - full_table_name.replace("`", "").encode("utf-8") - ).hexdigest() - ) + attr.format(full_table_name=sha1(full_table_name.replace("`", "").encode("utf-8")).hexdigest()) for attr in metadata_attr_sql ) @@ -332,12 +291,7 @@ def declare(full_table_name, definition, context): return ( "CREATE TABLE IF NOT EXISTS %s (\n" % full_table_name - + ",\n".join( - attribute_sql - + ["PRIMARY KEY (`" + "`,`".join(primary_key) + "`)"] - + foreign_key_sql - + index_sql - ) + + ",\n".join(attribute_sql + ["PRIMARY KEY (`" + "`,`".join(primary_key) + "`)"] + foreign_key_sql + index_sql) + '\n) ENGINE=InnoDB, COMMENT "%s"' % table_comment ), external_stores @@ -361,9 +315,7 @@ def _make_attribute_alter(new, old, primary_key): for v in new_names.values(): if v: if v in renamed: - raise DataJointError( - "Alter attempted to rename attribute {%s} twice." % v - ) + raise DataJointError("Alter attempted to rename attribute {%s} twice." % v) renamed.add(v) # verify that all renamed attributes existed in the old definition @@ -400,7 +352,9 @@ def _make_attribute_alter(new, old, primary_key): command=( "ADD" if (old_name or new_name) not in old_names - else "MODIFY" if not old_name else "CHANGE `%s`" % old_name + else "MODIFY" + if not old_name + else "CHANGE `%s`" % old_name ), new_def=new_def, after="" if after is None else "AFTER `%s`" % after, @@ -491,17 +445,13 @@ def substitute_special_type(match, category, foreign_key_sql, context): The filepath data type is disabled until complete validation. To turn it on as experimental feature, set the environment variable {env} = TRUE or upgrade datajoint. - """.format( - env=FILEPATH_FEATURE_SWITCH - ) + """.format(env=FILEPATH_FEATURE_SWITCH) ) match["store"] = match["type"].split("@", 1)[1] match["type"] = UUID_DATA_TYPE foreign_key_sql.append( "FOREIGN KEY (`{name}`) REFERENCES `{{database}}`.`{external_table_root}_{store}` (`hash`) " - "ON UPDATE RESTRICT ON DELETE RESTRICT".format( - external_table_root=EXTERNAL_TABLE_ROOT, **match - ) + "ON UPDATE RESTRICT ON DELETE RESTRICT".format(external_table_root=EXTERNAL_TABLE_ROOT, **match) ) elif category == "ADAPTED": adapter = get_adapter(context, match["type"]) @@ -540,38 +490,23 @@ def compile_attribute(line, in_key, foreign_key_sql, context): if match["nullable"]: if in_key: - raise DataJointError( - 'Primary key attributes cannot be nullable in line "%s"' % line - ) + raise DataJointError('Primary key attributes cannot be nullable in line "%s"' % line) match["default"] = "DEFAULT NULL" # nullable attributes default to null else: if match["default"]: - quote = ( - match["default"].split("(")[0].upper() not in CONSTANT_LITERALS - and match["default"][0] not in "\"'" - ) - match["default"] = ( - "NOT NULL DEFAULT " + ('"%s"' if quote else "%s") % match["default"] - ) + quote = match["default"].split("(")[0].upper() not in CONSTANT_LITERALS and match["default"][0] not in "\"'" + match["default"] = "NOT NULL DEFAULT " + ('"%s"' if quote else "%s") % match["default"] else: match["default"] = "NOT NULL" - match["comment"] = match["comment"].replace( - '"', '\\"' - ) # escape double quotes in comment + match["comment"] = match["comment"].replace('"', '\\"') # escape double quotes in comment if match["comment"].startswith(":"): - raise DataJointError( - 'An attribute comment must not start with a colon in comment "{comment}"'.format( - **match - ) - ) + raise DataJointError('An attribute comment must not start with a colon in comment "{comment}"'.format(**match)) category = match_type(match["type"]) if category in SPECIAL_TYPES: - match["comment"] = ":{type}:{comment}".format( - **match - ) # insert custom type into comment + match["comment"] = ":{type}:{comment}".format(**match) # insert custom type into comment substitute_special_type(match, category, foreign_key_sql, context) if category in SERIALIZED_TYPES and match["default"] not in { @@ -579,13 +514,8 @@ def compile_attribute(line, in_key, foreign_key_sql, context): "NOT NULL", }: raise DataJointError( - "The default value for a blob or attachment attributes can only be NULL in:\n{line}".format( - line=line - ) + "The default value for a blob or attachment attributes can only be NULL in:\n{line}".format(line=line) ) - sql = ( - "`{name}` {type} {default}" - + (' COMMENT "{comment}"' if match["comment"] else "") - ).format(**match) + sql = ("`{name}` {type} {default}" + (' COMMENT "{comment}"' if match["comment"] else "")).format(**match) return match["name"], sql, match.get("store") diff --git a/src/datajoint/dependencies.py b/src/datajoint/dependencies.py index 7496ee600..a342bf3f0 100644 --- a/src/datajoint/dependencies.py +++ b/src/datajoint/dependencies.py @@ -105,9 +105,7 @@ def load(self, force=True): concat('`', table_schema, '`.`', table_name, '`') as tab, column_name FROM information_schema.key_column_usage WHERE table_name not LIKE "~%%" AND table_schema in ('{schemas}') AND constraint_name="PRIMARY" - """.format( - schemas="','".join(self._conn.schemas) - ) + """.format(schemas="','".join(self._conn.schemas)) ) pks = defaultdict(set) for key in keys: @@ -129,9 +127,7 @@ def load(self, force=True): FROM information_schema.key_column_usage WHERE referenced_table_name NOT LIKE "~%%" AND (referenced_table_schema in ('{schemas}') OR referenced_table_schema is not NULL AND table_schema in ('{schemas}')) - """.format( - schemas="','".join(self._conn.schemas) - ), + """.format(schemas="','".join(self._conn.schemas)), as_dict=True, ) ) @@ -182,11 +178,7 @@ def parents(self, table_name, primary=None): :return: dict of tables referenced by the foreign keys of table """ self.load(force=False) - return { - p[0]: p[2] - for p in self.in_edges(table_name, data=True) - if primary is None or p[2]["primary"] == primary - } + return {p[0]: p[2] for p in self.in_edges(table_name, data=True) if primary is None or p[2]["primary"] == primary} def children(self, table_name, primary=None): """ @@ -197,11 +189,7 @@ def children(self, table_name, primary=None): :return: dict of tables referencing the table through foreign keys """ self.load(force=False) - return { - p[1]: p[2] - for p in self.out_edges(table_name, data=True) - if primary is None or p[2]["primary"] == primary - } + return {p[1]: p[2] for p in self.out_edges(table_name, data=True) if primary is None or p[2]["primary"] == primary} def descendants(self, full_table_name): """ diff --git a/src/datajoint/diagram.py b/src/datajoint/diagram.py index aa505fb54..cd6481044 100644 --- a/src/datajoint/diagram.py +++ b/src/datajoint/diagram.py @@ -39,9 +39,7 @@ class Diagram: """ def __init__(self, *args, **kwargs): - logger.warning( - "Please install matplotlib and pygraphviz libraries to enable the Diagram feature." - ) + logger.warning("Please install matplotlib and pygraphviz libraries to enable the Diagram feature.") else: @@ -72,7 +70,6 @@ class Diagram(nx.DiGraph): """ def __init__(self, source, context=None): - if isinstance(source, Diagram): # copy constructor self.nodes_to_show = set(source.nodes_to_show) @@ -95,9 +92,7 @@ def __init__(self, source, context=None): try: connection = source.schema.connection except AttributeError: - raise DataJointError( - "Could not find database connection in %s" % repr(source[0]) - ) + raise DataJointError("Could not find database connection in %s" % repr(source[0])) # initialize graph from dependencies connection.dependencies.load() @@ -114,9 +109,7 @@ def __init__(self, source, context=None): try: database = source.schema.database except AttributeError: - raise DataJointError( - "Cannot plot Diagram for %s" % repr(source) - ) + raise DataJointError("Cannot plot Diagram for %s" % repr(source)) for node in self: if node.startswith("`%s`" % database): self.nodes_to_show.add(node) @@ -145,17 +138,10 @@ def is_part(part, master): """ part = [s.strip("`") for s in part.split(".")] master = [s.strip("`") for s in master.split(".")] - return ( - master[0] == part[0] - and master[1] + "__" == part[1][: len(master[1]) + 2] - ) + return master[0] == part[0] and master[1] + "__" == part[1][: len(master[1]) + 2] self = Diagram(self) # copy - self.nodes_to_show.update( - n - for n in self.nodes() - if any(is_part(n, m) for m in self.nodes_to_show) - ) + self.nodes_to_show.update(n for n in self.nodes() if any(is_part(n, m) for m in self.nodes_to_show)) return self def __add__(self, arg): @@ -172,17 +158,11 @@ def __add__(self, arg): self.nodes_to_show.add(arg.full_table_name) except AttributeError: for i in range(arg): - new = nx.algorithms.boundary.node_boundary( - self, self.nodes_to_show - ) + new = nx.algorithms.boundary.node_boundary(self, self.nodes_to_show) if not new: break # add nodes referenced by aliased nodes - new.update( - nx.algorithms.boundary.node_boundary( - self, (a for a in new if a.isdigit()) - ) - ) + new.update(nx.algorithms.boundary.node_boundary(self, (a for a in new if a.isdigit()))) self.nodes_to_show.update(new) return self @@ -201,17 +181,11 @@ def __sub__(self, arg): except AttributeError: for i in range(arg): graph = nx.DiGraph(self).reverse() - new = nx.algorithms.boundary.node_boundary( - graph, self.nodes_to_show - ) + new = nx.algorithms.boundary.node_boundary(graph, self.nodes_to_show) if not new: break # add nodes referenced by aliased nodes - new.update( - nx.algorithms.boundary.node_boundary( - graph, (a for a in new if a.isdigit()) - ) - ) + new.update(nx.algorithms.boundary.node_boundary(graph, (a for a in new if a.isdigit()))) self.nodes_to_show.update(new) return self @@ -237,39 +211,24 @@ def _make_graph(self): # attributes for name in self.nodes_to_show: foreign_attributes = set( - attr - for p in self.in_edges(name, data=True) - for attr in p[2]["attr_map"] - if p[2]["primary"] + attr for p in self.in_edges(name, data=True) for attr in p[2]["attr_map"] if p[2]["primary"] ) self.nodes[name]["distinguished"] = ( - "primary_key" in self.nodes[name] - and foreign_attributes < self.nodes[name]["primary_key"] + "primary_key" in self.nodes[name] and foreign_attributes < self.nodes[name]["primary_key"] ) # include aliased nodes that are sandwiched between two displayed nodes - gaps = set( - nx.algorithms.boundary.node_boundary(self, self.nodes_to_show) - ).intersection( - nx.algorithms.boundary.node_boundary( - nx.DiGraph(self).reverse(), self.nodes_to_show - ) + gaps = set(nx.algorithms.boundary.node_boundary(self, self.nodes_to_show)).intersection( + nx.algorithms.boundary.node_boundary(nx.DiGraph(self).reverse(), self.nodes_to_show) ) nodes = self.nodes_to_show.union(a for a in gaps if a.isdigit) # construct subgraph and rename nodes to class names graph = nx.DiGraph(nx.DiGraph(self).subgraph(nodes)) - nx.set_node_attributes( - graph, name="node_type", values={n: _get_tier(n) for n in graph} - ) + nx.set_node_attributes(graph, name="node_type", values={n: _get_tier(n) for n in graph}) # relabel nodes to class names - mapping = { - node: lookup_class_name(node, self.context) or node - for node in graph.nodes() - } + mapping = {node: lookup_class_name(node, self.context) or node for node in graph.nodes()} new_names = [mapping.values()] if len(new_names) > len(set(new_names)): - raise DataJointError( - "Some classes have identical names. The Diagram cannot be plotted." - ) + raise DataJointError("Some classes have identical names. The Diagram cannot be plotted.") nx.relabel_nodes(graph, mapping, copy=False) return graph @@ -366,10 +325,7 @@ def make_dot(self): fixed=False, ), } - node_props = { - node: label_props[d["node_type"]] - for node, d in dict(graph.nodes(data=True)).items() - } + node_props = {node: label_props[d["node_type"]] for node, d in dict(graph.nodes(data=True)).items()} self._encapsulate_node_names(graph) self._encapsulate_edge_attributes(graph) @@ -390,24 +346,12 @@ def make_dot(self): assert issubclass(cls, Table) description = cls().describe(context=self.context).split("\n") description = ( - ( - "-" * 30 - if q.startswith("---") - else ( - q.replace("->", "→") - if "->" in q - else q.split(":")[0] - ) - ) + ("-" * 30 if q.startswith("---") else (q.replace("->", "→") if "->" in q else q.split(":")[0])) for q in description if not q.startswith("#") ) node.set_tooltip(" ".join(description)) - node.set_label( - "<" + name + ">" - if node.get("distinguished") == "True" - else name - ) + node.set_label("<" + name + ">" if node.get("distinguished") == "True" else name) node.set_color(props["color"]) node.set_style("filled") @@ -417,15 +361,10 @@ def make_dot(self): dest = edge.get_destination() props = graph.get_edge_data(src, dest) if props is None: - raise DataJointError( - "Could not find edge with source " - "'{}' and destination '{}'".format(src, dest) - ) + raise DataJointError("Could not find edge with source " "'{}' and destination '{}'".format(src, dest)) edge.set_color("#00000040") edge.set_style("solid" if props["primary"] else "dashed") - master_part = graph.nodes[dest][ - "node_type" - ] is Part and dest.startswith(src + ".") + master_part = graph.nodes[dest]["node_type"] is Part and dest.startswith(src + ".") edge.set_weight(3 if master_part else 1) edge.set_arrowhead("none") edge.set_penwidth(0.75 if props["multi"] else 2) diff --git a/src/datajoint/expression.py b/src/datajoint/expression.py index dd90087b8..b64cf070f 100644 --- a/src/datajoint/expression.py +++ b/src/datajoint/expression.py @@ -112,26 +112,16 @@ def from_clause(self): ) clause = next(support) for s, left in zip(support, self._left): - clause += " NATURAL{left} JOIN {clause}".format( - left=" LEFT" if left else "", clause=s - ) + clause += " NATURAL{left} JOIN {clause}".format(left=" LEFT" if left else "", clause=s) return clause def where_clause(self): - return ( - "" - if not self.restriction - else " WHERE (%s)" % ")AND(".join(str(s) for s in self.restriction) - ) + return "" if not self.restriction else " WHERE (%s)" % ")AND(".join(str(s) for s in self.restriction) def sorting_clauses(self): if not self._top: return "" - clause = ", ".join( - _wrap_attributes( - _flatten_attribute_list(self.primary_key, self._top.order_by) - ) - ) + clause = ", ".join(_wrap_attributes(_flatten_attribute_list(self.primary_key, self._top.order_by))) if clause: clause = f" ORDER BY {clause}" if self._top.limit is not None: @@ -210,9 +200,7 @@ def restrict(self, restriction): attributes = set() if isinstance(restriction, Top): result = ( - self.make_subquery() - if self._top and not self._top.__eq__(restriction) - else copy.copy(self) + self.make_subquery() if self._top and not self._top.__eq__(restriction) else copy.copy(self) ) # make subquery to avoid overwriting existing Top result._top = restriction return result @@ -222,25 +210,20 @@ def restrict(self, restriction): # check that all attributes in condition are present in the query try: raise DataJointError( - "Attribute `%s` is not found in query." - % next(attr for attr in attributes if attr not in self.heading.names) + "Attribute `%s` is not found in query." % next(attr for attr in attributes if attr not in self.heading.names) ) except StopIteration: pass # all ok # If the new condition uses any new attributes, a subquery is required. # However, Aggregation's HAVING statement works fine with aliased attributes. need_subquery = ( - isinstance(self, Union) - or (not isinstance(self, Aggregation) and self.heading.new_attributes) - or self._top + isinstance(self, Union) or (not isinstance(self, Aggregation) and self.heading.new_attributes) or self._top ) if need_subquery: result = self.make_subquery() else: result = copy.copy(self) - result._restriction = AndList( - self.restriction - ) # copy to preserve the original + result._restriction = AndList(self.restriction) # copy to preserve the original result.restriction.append(new_condition) result.restriction_attributes.update(attributes) return result @@ -318,8 +301,7 @@ def join(self, other, semantic_check=True, left=False): join_attributes = set(n for n in self.heading.names if n in other.heading.names) # needs subquery if self's FROM clause has common attributes with other's FROM clause need_subquery1 = need_subquery2 = bool( - (set(self.original_heading.names) & set(other.original_heading.names)) - - join_attributes + (set(self.original_heading.names) & set(other.original_heading.names)) - join_attributes ) # need subquery if any of the join attributes are derived need_subquery1 = ( @@ -374,35 +356,17 @@ def proj(self, *attributes, **named_attributes): from other attributes available before the projection. Each attribute name can only be used once. """ - named_attributes = { - k: translate_attribute(v)[1] for k, v in named_attributes.items() - } + named_attributes = {k: translate_attribute(v)[1] for k, v in named_attributes.items()} # new attributes in parentheses are included again with the new name without removing original - duplication_pattern = re.compile( - rf'^\s*\(\s*(?!{"|".join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*\)\s*$' - ) + duplication_pattern = re.compile(rf'^\s*\(\s*(?!{"|".join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*\)\s*$') # attributes without parentheses renamed - rename_pattern = re.compile( - rf'^\s*(?!{"|".join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*$' - ) + rename_pattern = re.compile(rf'^\s*(?!{"|".join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*$') replicate_map = { - k: m.group("name") - for k, m in ( - (k, duplication_pattern.match(v)) for k, v in named_attributes.items() - ) - if m - } - rename_map = { - k: m.group("name") - for k, m in ( - (k, rename_pattern.match(v)) for k, v in named_attributes.items() - ) - if m + k: m.group("name") for k, m in ((k, duplication_pattern.match(v)) for k, v in named_attributes.items()) if m } + rename_map = {k: m.group("name") for k, m in ((k, rename_pattern.match(v)) for k, v in named_attributes.items()) if m} compute_map = { - k: v - for k, v in named_attributes.items() - if not duplication_pattern.match(v) and not rename_pattern.match(v) + k: v for k, v in named_attributes.items() if not duplication_pattern.match(v) and not rename_pattern.match(v) } attributes = set(attributes) # include primary key @@ -411,16 +375,11 @@ def proj(self, *attributes, **named_attributes): if Ellipsis in attributes: attributes.discard(Ellipsis) attributes.update( - ( - a - for a in self.heading.secondary_attributes - if a not in attributes and a not in rename_map.values() - ) + (a for a in self.heading.secondary_attributes if a not in attributes and a not in rename_map.values()) ) try: raise DataJointError( - "%s is not a valid data type for an attribute in .proj" - % next(a for a in attributes if not isinstance(a, str)) + "%s is not a valid data type for an attribute in .proj" % next(a for a in attributes if not isinstance(a, str)) ) except StopIteration: pass # normal case @@ -438,20 +397,14 @@ def proj(self, *attributes, **named_attributes): pass # all ok # check that all attributes exist in heading try: - raise DataJointError( - "Attribute `%s` not found." - % next(a for a in attributes if a not in self.heading.names) - ) + raise DataJointError("Attribute `%s` not found." % next(a for a in attributes if a not in self.heading.names)) except StopIteration: pass # all ok # check that all mentioned names are present in heading mentions = attributes.union(replicate_map.values()).union(rename_map.values()) try: - raise DataJointError( - "Attribute '%s' not found." - % next(a for a in mentions if not self.heading.names) - ) + raise DataJointError("Attribute '%s' not found." % next(a for a in mentions if not self.heading.names)) except StopIteration: pass # all ok @@ -459,33 +412,21 @@ def proj(self, *attributes, **named_attributes): try: raise DataJointError( "Attribute `%s` already exists" - % next( - a - for a in rename_map - if a in attributes.union(compute_map).union(replicate_map) - ) + % next(a for a in rename_map if a in attributes.union(compute_map).union(replicate_map)) ) except StopIteration: pass # all ok try: raise DataJointError( "Attribute `%s` already exists" - % next( - a - for a in compute_map - if a in attributes.union(rename_map).union(replicate_map) - ) + % next(a for a in compute_map if a in attributes.union(rename_map).union(replicate_map)) ) except StopIteration: pass # all ok try: raise DataJointError( "Attribute `%s` already exists" - % next( - a - for a in replicate_map - if a in attributes.union(rename_map).union(compute_map) - ) + % next(a for a in replicate_map if a in attributes.union(rename_map).union(compute_map)) ) except StopIteration: pass # all ok @@ -495,15 +436,10 @@ def proj(self, *attributes, **named_attributes): used.update(rename_map.values()) used.update(replicate_map.values()) used.intersection_update(self.heading.names) - need_subquery = isinstance(self, Union) or any( - self.heading[name].attribute_expression is not None for name in used - ) + need_subquery = isinstance(self, Union) or any(self.heading[name].attribute_expression is not None for name in used) if not need_subquery and self.restriction: # need a subquery if the restriction applies to attributes that have been renamed - need_subquery = any( - name in self.restriction_attributes - for name in self.heading.new_attributes - ) + need_subquery = any(name in self.restriction_attributes for name in self.heading.new_attributes) result = self.make_subquery() if need_subquery else copy.copy(self) result._original_heading = result.original_heading @@ -529,9 +465,7 @@ def aggr(self, group, *attributes, keep_all_rows=False, **named_attributes): attributes = set(attributes) attributes.discard(Ellipsis) attributes.update(self.heading.secondary_attributes) - return Aggregation.create(self, group=group, keep_all_rows=keep_all_rows).proj( - *attributes, **named_attributes - ) + return Aggregation.create(self, group=group, keep_all_rows=keep_all_rows).proj(*attributes, **named_attributes) aggregate = aggr # alias for aggr @@ -575,9 +509,7 @@ def __len__(self): "count(*)" if any(result._left) else "count(DISTINCT {fields})".format( - fields=result.heading.as_sql( - result.primary_key, include_aliases=False - ) + fields=result.heading.as_sql(result.primary_key, include_aliases=False) ) ), from_=result.from_clause(), @@ -592,9 +524,7 @@ def __bool__(self): """ return bool( self.connection.query( - "SELECT EXISTS(SELECT 1 FROM {from_}{where})".format( - from_=self.from_clause(), where=self.where_clause() - ) + "SELECT EXISTS(SELECT 1 FROM {from_}{where})".format(from_=self.from_clause(), where=self.where_clause()) ).fetchone()[0] ) @@ -632,10 +562,7 @@ def __next__(self): key = self._iter_keys.pop(0) except AttributeError: # self._iter_keys is missing because __iter__ has not been called. - raise TypeError( - "A QueryExpression object is not an iterator. " - "Use iter(obj) to create an iterator." - ) + raise TypeError("A QueryExpression object is not an iterator. " "Use iter(obj) to create an iterator.") except IndexError: raise StopIteration else: @@ -666,11 +593,7 @@ def __repr__(self): :type self: :class:`QueryExpression` :rtype: str """ - return ( - super().__repr__() - if config["loglevel"].lower() == "debug" - else self.preview() - ) + return super().__repr__() if config["loglevel"].lower() == "debug" else self.preview() def preview(self, limit=None, width=None): """:return: a string of preview of the contents of the query.""" @@ -704,9 +627,7 @@ def create(cls, arg, group, keep_all_rows=False): join = arg.join(group, left=keep_all_rows) # reuse the join logic result = cls() result._connection = join.connection - result._heading = join.heading.set_primary_key( - arg.primary_key - ) # use left operand's primary key + result._heading = join.heading.set_primary_key(arg.primary_key) # use left operand's primary key result._support = join.support result._left = join._left result._left_restrict = join.restriction # WHERE clause applied before GROUP BY @@ -715,36 +636,26 @@ def create(cls, arg, group, keep_all_rows=False): return result def where_clause(self): - return ( - "" - if not self._left_restrict - else " WHERE (%s)" % ")AND(".join(str(s) for s in self._left_restrict) - ) + return "" if not self._left_restrict else " WHERE (%s)" % ")AND(".join(str(s) for s in self._left_restrict) def make_sql(self, fields=None): fields = self.heading.as_sql(fields or self.heading.names) assert self._grouping_attributes or not self.restriction distinct = set(self.heading.names) == set(self.primary_key) - return ( - "SELECT {distinct}{fields} FROM {from_}{where}{group_by}{sorting}".format( - distinct="DISTINCT " if distinct else "", - fields=fields, - from_=self.from_clause(), - where=self.where_clause(), - group_by=( - "" - if not self.primary_key - else ( - " GROUP BY `%s`" % "`,`".join(self._grouping_attributes) - + ( - "" - if not self.restriction - else " HAVING (%s)" % ")AND(".join(self.restriction) - ) - ) - ), - sorting=self.sorting_clauses(), - ) + return "SELECT {distinct}{fields} FROM {from_}{where}{group_by}{sorting}".format( + distinct="DISTINCT " if distinct else "", + fields=fields, + from_=self.from_clause(), + where=self.where_clause(), + group_by=( + "" + if not self.primary_key + else ( + " GROUP BY `%s`" % "`,`".join(self._grouping_attributes) + + ("" if not self.restriction else " HAVING (%s)" % ")AND(".join(self.restriction)) + ) + ), + sorting=self.sorting_clauses(), ) def __len__(self): @@ -755,9 +666,7 @@ def __len__(self): ).fetchone()[0] def __bool__(self): - return bool( - self.connection.query("SELECT EXISTS({sql})".format(sql=self.make_sql())) - ) + return bool(self.connection.query("SELECT EXISTS({sql})".format(sql=self.make_sql()))) class Union(QueryExpression): @@ -772,23 +681,13 @@ def create(cls, arg1, arg2): if inspect.isclass(arg2) and issubclass(arg2, QueryExpression): arg2 = arg2() # instantiate if a class if not isinstance(arg2, QueryExpression): - raise DataJointError( - "A QueryExpression can only be unioned with another QueryExpression" - ) + raise DataJointError("A QueryExpression can only be unioned with another QueryExpression") if arg1.connection != arg2.connection: - raise DataJointError( - "Cannot operate on QueryExpressions originating from different connections." - ) + raise DataJointError("Cannot operate on QueryExpressions originating from different connections.") if set(arg1.primary_key) != set(arg2.primary_key): - raise DataJointError( - "The operands of a union must share the same primary key." - ) - if set(arg1.heading.secondary_attributes) & set( - arg2.heading.secondary_attributes - ): - raise DataJointError( - "The operands of a union must not share any secondary attributes." - ) + raise DataJointError("The operands of a union must share the same primary key.") + if set(arg1.heading.secondary_attributes) & set(arg2.heading.secondary_attributes): + raise DataJointError("The operands of a union must not share any secondary attributes.") result = cls() result._connection = arg1.connection result._heading = arg1.heading.join(arg2.heading) @@ -797,34 +696,19 @@ def create(cls, arg1, arg2): def make_sql(self): arg1, arg2 = self._support - if ( - not arg1.heading.secondary_attributes - and not arg2.heading.secondary_attributes - ): + if not arg1.heading.secondary_attributes and not arg2.heading.secondary_attributes: # no secondary attributes: use UNION DISTINCT fields = arg1.primary_key return "SELECT * FROM (({sql1}) UNION ({sql2})) as `_u{alias}{sorting}`".format( - sql1=( - arg1.make_sql() - if isinstance(arg1, Union) - else arg1.make_sql(fields) - ), - sql2=( - arg2.make_sql() - if isinstance(arg2, Union) - else arg2.make_sql(fields) - ), + sql1=(arg1.make_sql() if isinstance(arg1, Union) else arg1.make_sql(fields)), + sql2=(arg2.make_sql() if isinstance(arg2, Union) else arg2.make_sql(fields)), alias=next(self.__count), sorting=self.sorting_clauses(), ) # with secondary attributes, use union of left join with antijoin fields = self.heading.names sql1 = arg1.join(arg2, left=True).make_sql(fields) - sql2 = ( - (arg2 - arg1) - .proj(..., **{k: "NULL" for k in arg1.heading.secondary_attributes}) - .make_sql(fields) - ) + sql2 = (arg2 - arg1).proj(..., **{k: "NULL" for k in arg1.heading.secondary_attributes}).make_sql(fields) return "({sql1}) UNION ({sql2})".format(sql1=sql1, sql2=sql2) def from_clause(self): @@ -844,9 +728,7 @@ def __len__(self): ).fetchone()[0] def __bool__(self): - return bool( - self.connection.query("SELECT EXISTS({sql})".format(sql=self.make_sql())) - ) + return bool(self.connection.query("SELECT EXISTS({sql})".format(sql=self.make_sql()))) class U: @@ -933,15 +815,13 @@ def join(self, other, left=False): raise DataJointError("Set U can only be joined with a QueryExpression.") try: raise DataJointError( - "Attribute `%s` not found" - % next(k for k in self.primary_key if k not in other.heading.names) + "Attribute `%s` not found" % next(k for k in self.primary_key if k not in other.heading.names) ) except StopIteration: pass # all ok result = copy.copy(other) result._heading = result.heading.set_primary_key( - other.primary_key - + [k for k in self.primary_key if k not in other.primary_key] + other.primary_key + [k for k in self.primary_key if k not in other.primary_key] ) return result @@ -959,12 +839,8 @@ def aggr(self, group, **named_attributes): :return: The derived query expression """ if named_attributes.get("keep_all_rows", False): - raise DataJointError( - "Cannot set keep_all_rows=True when aggregating on a universal set." - ) - return Aggregation.create(self, group=group, keep_all_rows=False).proj( - **named_attributes - ) + raise DataJointError("Cannot set keep_all_rows=True when aggregating on a universal set.") + return Aggregation.create(self, group=group, keep_all_rows=False).proj(**named_attributes) aggregate = aggr # alias for aggr diff --git a/src/datajoint/external.py b/src/datajoint/external.py index b3de2ff5d..583ef24e4 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -26,11 +26,7 @@ def subfold(name, folds): """ subfolding for external storage: e.g. subfold('aBCdefg', (2, 3)) --> ['ab','cde'] """ - return ( - (name[: folds[0]].lower(),) + subfold(name[folds[0] :], folds[1:]) - if folds - else () - ) + return (name[: folds[0]].lower(),) + subfold(name[folds[0] :], folds[1:]) if folds else () class ExternalTable(Table): @@ -58,9 +54,7 @@ def __init__(self, connection, store, database): self.declare() self._s3 = None if self.spec["protocol"] == "file" and not Path(self.spec["location"]).is_dir(): - raise FileNotFoundError( - "Inaccessible local directory %s" % self.spec["location"] - ) from None + raise FileNotFoundError("Inaccessible local directory %s" % self.spec["location"]) from None @property def definition(self): @@ -94,8 +88,7 @@ def _make_external_filepath(self, relative_filepath): posix_path = PurePosixPath(PureWindowsPath(self.spec["location"])) location_path = ( Path(*posix_path.parts[1:]) - if len(self.spec["location"]) > 0 - and any(case in posix_path.parts[0] for case in ("\\", ":")) + if len(self.spec["location"]) > 0 and any(case in posix_path.parts[0] for case in ("\\", ":")) else Path(posix_path) ) return PurePosixPath(location_path, relative_filepath) @@ -146,9 +139,7 @@ def _download_buffer(self, external_path): try: return Path(external_path).read_bytes() except FileNotFoundError: - raise errors.MissingExternalFile( - f"Missing external file {external_path}" - ) from None + raise errors.MissingExternalFile(f"Missing external file {external_path}") from None assert False def _remove_external_file(self, external_path): @@ -180,8 +171,7 @@ def put(self, blob): self._upload_buffer(blob, self._make_uuid_path(uuid)) # insert tracking info self.connection.query( - "INSERT INTO {tab} (hash, size) VALUES (%s, {size}) ON DUPLICATE KEY " - "UPDATE timestamp=CURRENT_TIMESTAMP".format( + "INSERT INTO {tab} (hash, size) VALUES (%s, {size}) ON DUPLICATE KEY " "UPDATE timestamp=CURRENT_TIMESTAMP".format( tab=self.full_table_name, size=len(blob) ), args=(uuid.bytes,), @@ -212,14 +202,10 @@ def get(self, uuid): if not SUPPORT_MIGRATED_BLOBS: raise # blobs migrated from datajoint 0.11 are stored at explicitly defined filepaths - relative_filepath, contents_hash = (self & {"hash": uuid}).fetch1( - "filepath", "contents_hash" - ) + relative_filepath, contents_hash = (self & {"hash": uuid}).fetch1("filepath", "contents_hash") if relative_filepath is None: raise - blob = self._download_buffer( - self._make_external_filepath(relative_filepath) - ) + blob = self._download_buffer(self._make_external_filepath(relative_filepath)) if cache_folder: cache_path.mkdir(parents=True, exist_ok=True) safe_write(cache_path / uuid.hex, blob) @@ -264,18 +250,10 @@ def upload_filepath(self, local_filepath): """ local_filepath = Path(local_filepath) try: - relative_filepath = str( - local_filepath.relative_to(self.spec["stage"]).as_posix() - ) + relative_filepath = str(local_filepath.relative_to(self.spec["stage"]).as_posix()) except ValueError: - raise DataJointError( - "The path {path} is not in stage {stage}".format( - path=local_filepath.parent, **self.spec - ) - ) - uuid = uuid_from_buffer( - init_string=relative_filepath - ) # hash relative path, not contents + raise DataJointError("The path {path} is not in stage {stage}".format(path=local_filepath.parent, **self.spec)) + uuid = uuid_from_buffer(init_string=relative_filepath) # hash relative path, not contents contents_hash = uuid_from_file(local_filepath) # check if the remote file already exists and verify that it matches @@ -283,9 +261,7 @@ def upload_filepath(self, local_filepath): if check_hash.size: # the tracking entry exists, check that it's the same file as before if contents_hash != check_hash[0]: - raise DataJointError( - f"A different version of '{relative_filepath}' has already been placed." - ) + raise DataJointError(f"A different version of '{relative_filepath}' has already been placed.") else: # upload the file and create its tracking entry self._upload_file( @@ -316,37 +292,27 @@ def _need_checksum(local_filepath, expected_size): actual_size = Path(local_filepath).stat().st_size if expected_size != actual_size: # this should never happen without outside interference - raise DataJointError( - f"'{local_filepath}' downloaded but size did not match." - ) + raise DataJointError(f"'{local_filepath}' downloaded but size did not match.") return limit is None or actual_size < limit if filepath_hash is not None: - relative_filepath, contents_hash, size = ( - self & {"hash": filepath_hash} - ).fetch1("filepath", "contents_hash", "size") + relative_filepath, contents_hash, size = (self & {"hash": filepath_hash}).fetch1( + "filepath", "contents_hash", "size" + ) external_path = self._make_external_filepath(relative_filepath) local_filepath = Path(self.spec["stage"]).absolute() / relative_filepath file_exists = Path(local_filepath).is_file() and ( - not _need_checksum(local_filepath, size) - or uuid_from_file(local_filepath) == contents_hash + not _need_checksum(local_filepath, size) or uuid_from_file(local_filepath) == contents_hash ) if not file_exists: self._download_file(external_path, local_filepath) - if ( - _need_checksum(local_filepath, size) - and uuid_from_file(local_filepath) != contents_hash - ): + if _need_checksum(local_filepath, size) and uuid_from_file(local_filepath) != contents_hash: # this should never happen without outside interference - raise DataJointError( - f"'{local_filepath}' downloaded but did not pass checksum." - ) + raise DataJointError(f"'{local_filepath}' downloaded but did not pass checksum.") if not _need_checksum(local_filepath, size): - logger.warning( - f"Skipped checksum for file with hash: {contents_hash}, and path: {local_filepath}" - ) + logger.warning(f"Skipped checksum for file with hash: {contents_hash}, and path: {local_filepath}") return str(local_filepath), contents_hash # --- UTILITIES --- @@ -363,9 +329,7 @@ def references(self): SELECT concat('`', table_schema, '`.`', table_name, '`') as referencing_table, column_name FROM information_schema.key_column_usage WHERE referenced_table_name="{tab}" and referenced_table_schema="{db}" - """.format( - tab=self.table_name, db=self.database - ), + """.format(tab=self.table_name, db=self.database), as_dict=True, ) ) @@ -399,10 +363,7 @@ def unused(self): :return: self restricted to elements that are not in use by any tables in the schema """ return self - [ - FreeTable(self.connection, ref["referencing_table"]).proj( - hash=ref["column_name"] - ) - for ref in self.references + FreeTable(self.connection, ref["referencing_table"]).proj(hash=ref["column_name"]) for ref in self.references ] def used(self): @@ -412,10 +373,7 @@ def used(self): :return: self restricted to elements that in use by tables in the schema """ return self & [ - FreeTable(self.connection, ref["referencing_table"]).proj( - hash=ref["column_name"] - ) - for ref in self.references + FreeTable(self.connection, ref["referencing_table"]).proj(hash=ref["column_name"]) for ref in self.references ] def delete( @@ -436,10 +394,7 @@ def delete( :return: if deleting external files, returns errors """ if delete_external_files not in (True, False): - raise DataJointError( - "The delete_external_files argument must be set to either " - "True or False in delete()" - ) + raise DataJointError("The delete_external_files argument must be set to either " "True or False in delete()") if not delete_external_files: self.unused().delete_quick() @@ -485,11 +440,8 @@ def __init__(self, schema): self._tables = {} def __repr__(self): - return "External file tables for schema `{schema}`:\n ".format( - schema=self.schema.database - ) + "\n ".join( - '"{store}" {protocol}:{location}'.format(store=k, **v.spec) - for k, v in self.items() + return "External file tables for schema `{schema}`:\n ".format(schema=self.schema.database) + "\n ".join( + '"{store}" {protocol}:{location}'.format(store=k, **v.spec) for k, v in self.items() ) def __getitem__(self, store): diff --git a/src/datajoint/fetch.py b/src/datajoint/fetch.py index 1c9b811f1..278a9c3f2 100644 --- a/src/datajoint/fetch.py +++ b/src/datajoint/fetch.py @@ -51,11 +51,7 @@ def _get(connection, attr, data, squeeze, download_path): if attr.json: return json.loads(data) - extern = ( - connection.schemas[attr.database].external[attr.store] - if attr.is_external - else None - ) + extern = connection.schemas[attr.database].external[attr.store] if attr.is_external else None # apply attribute adapter if present adapt = attr.adapter.get if attr.adapter else lambda x: x @@ -69,33 +65,19 @@ def _get(connection, attr, data, squeeze, download_path): # 3. if exists and checksum passes then return the local filepath # 4. Otherwise, download the remote file and return the new filepath _uuid = uuid.UUID(bytes=data) if attr.is_external else None - attachment_name = ( - extern.get_attachment_name(_uuid) - if attr.is_external - else data.split(b"\0", 1)[0].decode() - ) + attachment_name = extern.get_attachment_name(_uuid) if attr.is_external else data.split(b"\0", 1)[0].decode() local_filepath = Path(download_path) / attachment_name if local_filepath.is_file(): - attachment_checksum = ( - _uuid if attr.is_external else hash.uuid_from_buffer(data) - ) - if attachment_checksum == hash.uuid_from_file( - local_filepath, init_string=attachment_name + "\0" - ): - return adapt( - str(local_filepath) - ) # checksum passed, no need to download again + attachment_checksum = _uuid if attr.is_external else hash.uuid_from_buffer(data) + if attachment_checksum == hash.uuid_from_file(local_filepath, init_string=attachment_name + "\0"): + return adapt(str(local_filepath)) # checksum passed, no need to download again # generate the next available alias filename for n in itertools.count(): - f = local_filepath.parent / ( - local_filepath.stem + "_%04x" % n + local_filepath.suffix - ) + f = local_filepath.parent / (local_filepath.stem + "_%04x" % n + local_filepath.suffix) if not f.is_file(): local_filepath = f break - if attachment_checksum == hash.uuid_from_file( - f, init_string=attachment_name + "\0" - ): + if attachment_checksum == hash.uuid_from_file(f, init_string=attachment_name + "\0"): return adapt(str(f)) # checksum passed, no need to download again # Save attachment if attr.is_external: @@ -172,29 +154,22 @@ def __call__( if attrs_as_dict: # absorb KEY into attrs and prepare to return attributes as dict (issue #595) if any(is_key(k) for k in attrs): - attrs = list(self._expression.primary_key) + [ - a for a in attrs if a not in self._expression.primary_key - ] + attrs = list(self._expression.primary_key) + [a for a in attrs if a not in self._expression.primary_key] if as_dict is None: as_dict = bool(attrs) # default to True for "KEY" and False otherwise # format should not be specified with attrs or is_dict=True if format is not None and (as_dict or attrs): raise DataJointError( - "Cannot specify output format when as_dict=True or " - "when attributes are selected to be fetched separately." + "Cannot specify output format when as_dict=True or " "when attributes are selected to be fetched separately." ) if format not in {None, "array", "frame"}: - raise DataJointError( - "Fetch output format must be in " - '{{"array", "frame"}} but "{}" was given'.format(format) - ) + raise DataJointError("Fetch output format must be in " '{{"array", "frame"}} but "{}" was given'.format(format)) if not (attrs or as_dict) and format is None: format = config["fetch_format"] # default to array if format not in {"array", "frame"}: raise DataJointError( - 'Invalid entry "{}" in datajoint.config["fetch_format"]: ' - 'use "array" or "frame"'.format(format) + 'Invalid entry "{}" in datajoint.config["fetch_format"]: ' 'use "array" or "frame"'.format(format) ) get = partial( @@ -216,18 +191,11 @@ def __call__( format="array", ) if attrs_as_dict: - ret = [ - {k: v for k, v in zip(ret.dtype.names, x) if k in attrs} - for x in ret - ] + ret = [{k: v for k, v in zip(ret.dtype.names, x) if k in attrs} for x in ret] else: return_values = [ ( - list( - (to_dicts if as_dict else lambda x: x)( - ret[self._expression.primary_key] - ) - ) + list((to_dicts if as_dict else lambda x: x)(ret[self._expression.primary_key])) if is_key(attribute) else ret[attribute] ) @@ -238,10 +206,7 @@ def __call__( cur = self._expression.cursor(as_dict=as_dict) heading = self._expression.heading if as_dict: - ret = [ - dict((name, get(heading[name], d[name])) for name in heading.names) - for d in cur - ] + ret = [dict((name, get(heading[name], d[name])) for name in heading.names) for d in cur] else: ret = list(cur.fetchall()) record_type = ( @@ -254,8 +219,7 @@ def __call__( name, type(value), ) # use the first element to determine blob type - if heading[name].is_blob - and isinstance(value, numbers.Number) + if heading[name].is_blob and isinstance(value, numbers.Number) else (name, heading.as_dtype[name]) ) for value, name in zip(ret[0], heading.as_dtype.names) @@ -307,9 +271,7 @@ def __call__(self, *attrs, squeeze=False, download_path="."): cur = self._expression.cursor(as_dict=True) ret = cur.fetchone() if not ret or cur.fetchone(): - raise DataJointError( - "fetch1 requires exactly one tuple in the input set." - ) + raise DataJointError("fetch1 requires exactly one tuple in the input set.") ret = dict( ( name, @@ -325,19 +287,11 @@ def __call__(self, *attrs, squeeze=False, download_path="."): ) else: # fetch some attributes, return as tuple attributes = [a for a in attrs if not is_key(a)] - result = self._expression.proj(*attributes).fetch( - squeeze=squeeze, download_path=download_path, format="array" - ) + result = self._expression.proj(*attributes).fetch(squeeze=squeeze, download_path=download_path, format="array") if len(result) != 1: - raise DataJointError( - "fetch1 should only return one tuple. %d tuples found" % len(result) - ) + raise DataJointError("fetch1 should only return one tuple. %d tuples found" % len(result)) return_values = tuple( - ( - next(to_dicts(result[self._expression.primary_key])) - if is_key(attribute) - else result[attribute][0] - ) + (next(to_dicts(result[self._expression.primary_key])) if is_key(attribute) else result[attribute][0]) for attribute in attrs ) ret = return_values[0] if len(attrs) == 1 else return_values diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index c81b5a61a..fcc21e019 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -17,31 +17,29 @@ logger = logging.getLogger(__name__.split(".")[0]) -default_attribute_properties = ( - dict( # these default values are set in computed attributes - name=None, - type="expression", - in_key=False, - nullable=False, - default=None, - comment="calculated attribute", - autoincrement=False, - numeric=None, - string=None, - uuid=False, - json=None, - is_blob=False, - is_attachment=False, - is_filepath=False, - is_external=False, - is_hidden=False, - adapter=None, - store=None, - unsupported=False, - attribute_expression=None, - database=None, - dtype=object, - ) +default_attribute_properties = dict( # these default values are set in computed attributes + name=None, + type="expression", + in_key=False, + nullable=False, + default=None, + comment="calculated attribute", + autoincrement=False, + numeric=None, + string=None, + uuid=False, + json=None, + is_blob=False, + is_attachment=False, + is_filepath=False, + is_external=False, + is_hidden=False, + adapter=None, + store=None, + unsupported=False, + attribute_expression=None, + database=None, + dtype=object, ) @@ -101,11 +99,7 @@ def __init__(self, attribute_specs=None, table_info=None): self.indexes = None self.table_info = table_info self._table_status = None - self._attributes = ( - None - if attribute_specs is None - else dict((q["name"], Attribute(**q)) for q in attribute_specs) - ) + self._attributes = None if attribute_specs is None else dict((q["name"], Attribute(**q)) for q in attribute_specs) def __len__(self): return 0 if self.attributes is None else len(self.attributes) @@ -142,17 +136,11 @@ def blobs(self): @property def non_blobs(self): - return [ - k - for k, v in self.attributes.items() - if not (v.is_blob or v.is_attachment or v.is_filepath or v.json) - ] + return [k for k, v in self.attributes.items() if not (v.is_blob or v.is_attachment or v.is_filepath or v.json)] @property def new_attributes(self): - return [ - k for k, v in self.attributes.items() if v.attribute_expression is not None - ] + return [k for k, v in self.attributes.items() if v.attribute_expression is not None] def __getitem__(self, name): """shortcut to the attribute""" @@ -186,9 +174,7 @@ def as_dtype(self): """ represent the heading as a numpy dtype """ - return np.dtype( - dict(names=self.names, formats=[v.dtype for v in self.attributes.values()]) - ) + return np.dtype(dict(names=self.names, formats=[v.dtype for v in self.attributes.values()])) def as_sql(self, fields, include_aliases=True): """ @@ -198,8 +184,7 @@ def as_sql(self, fields, include_aliases=True): ( "`%s`" % name if self.attributes[name].attribute_expression is None - else self.attributes[name].attribute_expression - + (" as `%s`" % name if include_aliases else "") + else self.attributes[name].attribute_expression + (" as `%s`" % name if include_aliases else "") ) for name in fields ) @@ -209,13 +194,9 @@ def __iter__(self): def _init_from_database(self): """initialize heading from an existing database table.""" - conn, database, table_name, context = ( - self.table_info[k] for k in ("conn", "database", "table_name", "context") - ) + conn, database, table_name, context = (self.table_info[k] for k in ("conn", "database", "table_name", "context")) info = conn.query( - 'SHOW TABLE STATUS FROM `{database}` WHERE name="{table_name}"'.format( - table_name=table_name, database=database - ), + 'SHOW TABLE STATUS FROM `{database}` WHERE name="{table_name}"'.format(table_name=table_name, database=database), as_dict=True, ).fetchone() if info is None: @@ -223,15 +204,11 @@ def _init_from_database(self): logger.warning("Could not create the ~log table") return raise DataJointError( - "The table `{database}`.`{table_name}` is not defined.".format( - table_name=table_name, database=database - ) + "The table `{database}`.`{table_name}` is not defined.".format(table_name=table_name, database=database) ) self._table_status = {k.lower(): v for k, v in info.items()} cur = conn.query( - "SHOW FULL COLUMNS FROM `{table_name}` IN `{database}`".format( - table_name=table_name, database=database - ), + "SHOW FULL COLUMNS FROM `{table_name}` IN `{database}`".format(table_name=table_name, database=database), as_dict=True, ) @@ -250,12 +227,7 @@ def _init_from_database(self): # rename and drop attributes attributes = [ - { - rename_map[k] if k in rename_map else k: v - for k, v in x.items() - if k not in fields_to_drop - } - for x in attributes + {rename_map[k] if k in rename_map else k: v for k, v in x.items() if k not in fields_to_drop} for x in attributes ] numeric_types = { ("float", False): np.float64, @@ -282,17 +254,9 @@ def _init_from_database(self): in_key=(attr["in_key"] == "PRI"), database=database, nullable=attr["nullable"] == "YES", - autoincrement=bool( - re.search(r"auto_increment", attr["Extra"], flags=re.I) - ), - numeric=any( - TYPE_PATTERN[t].match(attr["type"]) - for t in ("DECIMAL", "INTEGER", "FLOAT") - ), - string=any( - TYPE_PATTERN[t].match(attr["type"]) - for t in ("ENUM", "TEMPORAL", "STRING") - ), + autoincrement=bool(re.search(r"auto_increment", attr["Extra"], flags=re.I)), + numeric=any(TYPE_PATTERN[t].match(attr["type"]) for t in ("DECIMAL", "INTEGER", "FLOAT")), + string=any(TYPE_PATTERN[t].match(attr["type"]) for t in ("ENUM", "TEMPORAL", "STRING")), is_blob=bool(TYPE_PATTERN["INTERNAL_BLOB"].match(attr["type"])), uuid=False, json=bool(TYPE_PATTERN["JSON"].match(attr["type"])), @@ -306,12 +270,8 @@ def _init_from_database(self): ) if any(TYPE_PATTERN[t].match(attr["type"]) for t in ("INTEGER", "FLOAT")): - attr["type"] = re.sub( - r"\(\d+\)", "", attr["type"], count=1 - ) # strip size off integers and floats - attr["unsupported"] = not any( - (attr["is_blob"], attr["numeric"], attr["numeric"]) - ) + attr["type"] = re.sub(r"\(\d+\)", "", attr["type"], count=1) # strip size off integers and floats + attr["unsupported"] = not any((attr["is_blob"], attr["numeric"], attr["numeric"])) attr.pop("Extra") # process custom DataJoint types @@ -336,15 +296,11 @@ def _init_from_database(self): adapter_name=adapter_name, **attr ) ) - special = not any( - TYPE_PATTERN[c].match(attr["type"]) for c in NATIVE_TYPES - ) + special = not any(TYPE_PATTERN[c].match(attr["type"]) for c in NATIVE_TYPES) if special: try: - category = next( - c for c in SPECIAL_TYPES if TYPE_PATTERN[c].match(attr["type"]) - ) + category = next(c for c in SPECIAL_TYPES if TYPE_PATTERN[c].match(attr["type"])) except StopIteration: if attr["type"].startswith("external"): url = ( @@ -352,21 +308,18 @@ def _init_from_database(self): "#migration-between-datajoint-v0-11-and-v0-12" ) raise DataJointError( - "Legacy datatype `{type}`. Migrate your external stores to " - "datajoint 0.12: {url}".format(url=url, **attr) + "Legacy datatype `{type}`. Migrate your external stores to " "datajoint 0.12: {url}".format( + url=url, **attr + ) ) - raise DataJointError( - "Unknown attribute type `{type}`".format(**attr) - ) + raise DataJointError("Unknown attribute type `{type}`".format(**attr)) if category == "FILEPATH" and not _support_filepath_types(): raise DataJointError( """ The filepath data type is disabled until complete validation. To turn it on as experimental feature, set the environment variable {env} = TRUE or upgrade datajoint. - """.format( - env=FILEPATH_FEATURE_SWITCH - ) + """.format(env=FILEPATH_FEATURE_SWITCH) ) attr.update( unsupported=False, @@ -376,11 +329,7 @@ def _init_from_database(self): is_blob=category in ("INTERNAL_BLOB", "EXTERNAL_BLOB"), uuid=category == "UUID", is_external=category in EXTERNAL_TYPES, - store=( - attr["type"].split("@")[1] - if category in EXTERNAL_TYPES - else None - ), + store=(attr["type"].split("@")[1] if category in EXTERNAL_TYPES else None), ) if attr["in_key"] and any( @@ -391,15 +340,9 @@ def _init_from_database(self): attr["json"], ) ): - raise DataJointError( - "Json, Blob, attachment, or filepath attributes are not allowed in the primary key" - ) + raise DataJointError("Json, Blob, attachment, or filepath attributes are not allowed in the primary key") - if ( - attr["string"] - and attr["default"] is not None - and attr["default"] not in sql_literals - ): + if attr["string"] and attr["default"] is not None and attr["default"] not in sql_literals: attr["default"] = '"%s"' % attr["default"] if attr["nullable"]: # nullable fields always default to null @@ -414,9 +357,7 @@ def _init_from_database(self): is_unsigned = bool(re.match("sunsigned", attr["type"], flags=re.I)) t = re.sub(r"\(.*\)", "", attr["type"]) # remove parentheses t = re.sub(r" unsigned$", "", t) # remove unsigned - assert (t, is_unsigned) in numeric_types, ( - "dtype not found for type %s" % t - ) + assert (t, is_unsigned) in numeric_types, "dtype not found for type %s" % t attr["dtype"] = numeric_types[(t, is_unsigned)] if attr["adapter"]: @@ -433,8 +374,7 @@ def _init_from_database(self): ): if item["Key_name"] != "PRIMARY": keys[item["Key_name"]][item["Seq_in_index"]] = dict( - column=item["Column_name"] - or f"({item['Expression']})".replace(r"\'", "'"), + column=item["Column_name"] or f"({item['Expression']})".replace(r"\'", "'"), unique=(item["Non_unique"] == 0), nullable=item["Null"].lower() == "yes", ) @@ -486,21 +426,9 @@ def join(self, other): """ return Heading( [self.attributes[name].todict() for name in self.primary_key] - + [ - other.attributes[name].todict() - for name in other.primary_key - if name not in self.primary_key - ] - + [ - self.attributes[name].todict() - for name in self.secondary_attributes - if name not in other.primary_key - ] - + [ - other.attributes[name].todict() - for name in other.secondary_attributes - if name not in self.primary_key - ] + + [other.attributes[name].todict() for name in other.primary_key if name not in self.primary_key] + + [self.attributes[name].todict() for name in self.secondary_attributes if name not in other.primary_key] + + [other.attributes[name].todict() for name in other.secondary_attributes if name not in self.primary_key] ) def set_primary_key(self, primary_key): @@ -510,15 +438,8 @@ def set_primary_key(self, primary_key): """ return Heading( chain( - ( - dict(self.attributes[name].todict(), in_key=True) - for name in primary_key - ), - ( - dict(self.attributes[name].todict(), in_key=False) - for name in self.names - if name not in primary_key - ), + (dict(self.attributes[name].todict(), in_key=True) for name in primary_key), + (dict(self.attributes[name].todict(), in_key=False) for name in self.names if name not in primary_key), ) ) @@ -527,7 +448,4 @@ def make_subquery_heading(self): Create a new heading with removed attribute sql_expressions. Used by subqueries, which resolve the sql_expressions. """ - return Heading( - dict(v.todict(), attribute_expression=None) - for v in self.attributes.values() - ) + return Heading(dict(v.todict(), attribute_expression=None) for v in self.attributes.values()) diff --git a/src/datajoint/jobs.py b/src/datajoint/jobs.py index d6b31e13e..dc568f256 100644 --- a/src/datajoint/jobs.py +++ b/src/datajoint/jobs.py @@ -19,11 +19,7 @@ class JobTable(Table): def __init__(self, conn, database): self.database = database self._connection = conn - self._heading = Heading( - table_info=dict( - conn=conn, database=database, table_name=self.table_name, context=None - ) - ) + self._heading = Heading(table_info=dict(conn=conn, database=database, table_name=self.table_name, context=None)) self._support = [self.full_table_name] self._definition = """ # job reservation table for `{database}` @@ -39,9 +35,7 @@ def __init__(self, conn, database): pid=0 :int unsigned # system process id connection_id = 0 : bigint unsigned # connection_id() timestamp=CURRENT_TIMESTAMP :timestamp # automatic timestamp - """.format( - database=database, error_message_length=ERROR_MESSAGE_LENGTH - ) + """.format(database=database, error_message_length=ERROR_MESSAGE_LENGTH) if not self.is_declared: self.declare() self._user = self.connection.get_user() @@ -140,10 +134,7 @@ def error(self, table_name, key, error_message, error_stack=None): :param error_stack: stack trace """ if len(error_message) > ERROR_MESSAGE_LENGTH: - error_message = ( - error_message[: ERROR_MESSAGE_LENGTH - len(TRUNCATION_APPENDIX)] - + TRUNCATION_APPENDIX - ) + error_message = error_message[: ERROR_MESSAGE_LENGTH - len(TRUNCATION_APPENDIX)] + TRUNCATION_APPENDIX with config(enable_python_native_blobs=True): self.insert1( dict( diff --git a/src/datajoint/preview.py b/src/datajoint/preview.py index 564c92a0a..4fd0d1fe5 100644 --- a/src/datajoint/preview.py +++ b/src/datajoint/preview.py @@ -16,31 +16,18 @@ def preview(query_expression, limit, width): columns = heading.names widths = { f: min( - max( - [len(f)] + [len(str(e)) for e in tuples[f]] - if f in tuples.dtype.names - else [len("=BLOB=")] - ) - + 4, + max([len(f)] + [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else [len("=BLOB=")]) + 4, width, ) for f in columns } templates = {f: "%%-%d.%ds" % (widths[f], widths[f]) for f in columns} return ( - " ".join( - [templates[f] % ("*" + f if f in rel.primary_key else f) for f in columns] - ) + " ".join([templates[f] % ("*" + f if f in rel.primary_key else f) for f in columns]) + "\n" + " ".join(["+" + "-" * (widths[column] - 2) + "+" for column in columns]) + "\n" - + "\n".join( - " ".join( - templates[f] % (tup[f] if f in tup.dtype.names else "=BLOB=") - for f in columns - ) - for tup in tuples - ) + + "\n".join(" ".join(templates[f] % (tup[f] if f in tup.dtype.names else "=BLOB=") for f in columns) for tup in tuples) + ("\n ...\n" if has_more else "\n") + (" (Total: %d)\n" % len(rel) if config["display.show_tuple_count"] else "") ) @@ -126,28 +113,16 @@ def repr_html(query_expression): head_template.format( column=c, comment=heading.attributes[c].comment, - primary=( - "primary" if c in query_expression.primary_key else "nonprimary" - ), + primary=("primary" if c in query_expression.primary_key else "nonprimary"), ) for c in heading.names ), ellipsis="

...

" if has_more else "", body="".join( [ - "\n".join( - [ - "%s" - % (tup[name] if name in tup.dtype.names else "=BLOB=") - for name in heading.names - ] - ) + "\n".join(["%s" % (tup[name] if name in tup.dtype.names else "=BLOB=") for name in heading.names]) for tup in tuples ] ), - count=( - ("

Total: %d

" % len(rel)) - if config["display.show_tuple_count"] - else "" - ), + count=(("

Total: %d

" % len(rel)) if config["display.show_tuple_count"] else ""), ) diff --git a/src/datajoint/s3.py b/src/datajoint/s3.py index 98dc75708..e107a7f4b 100644 --- a/src/datajoint/s3.py +++ b/src/datajoint/s3.py @@ -58,15 +58,11 @@ def __init__( def put(self, name, buffer): logger.debug("put: {}:{}".format(self.bucket, name)) - return self.client.put_object( - self.bucket, str(name), BytesIO(buffer), length=len(buffer) - ) + return self.client.put_object(self.bucket, str(name), BytesIO(buffer), length=len(buffer)) def fput(self, local_file, name, metadata=None): logger.debug("fput: {} -> {}:{}".format(self.bucket, local_file, name)) - return self.client.fput_object( - self.bucket, str(name), str(local_file), metadata=metadata - ) + return self.client.fput_object(self.bucket, str(name), str(local_file), metadata=metadata) def get(self, name): logger.debug("get: {}:{}".format(self.bucket, name)) diff --git a/src/datajoint/schemas.py b/src/datajoint/schemas.py index 8cb7a3668..095e6fdc6 100644 --- a/src/datajoint/schemas.py +++ b/src/datajoint/schemas.py @@ -109,11 +109,7 @@ def activate( if self.database is not None and self.exists: if self.database == schema_name: # already activated return - raise DataJointError( - "The schema is already activated for schema {db}.".format( - db=self.database - ) - ) + raise DataJointError("The schema is already activated for schema {db}.".format(db=self.database)) if connection is not None: self.connection = connection if self.connection is None: @@ -128,21 +124,17 @@ def activate( if not self.exists: if not self.create_schema or not self.database: raise DataJointError( - "Database `{name}` has not yet been declared. " - "Set argument create_schema=True to create it.".format( + "Database `{name}` has not yet been declared. " "Set argument create_schema=True to create it.".format( name=schema_name ) ) # create database logger.debug("Creating schema `{name}`.".format(name=schema_name)) try: - self.connection.query( - "CREATE DATABASE `{name}`".format(name=schema_name) - ) + self.connection.query("CREATE DATABASE `{name}`".format(name=schema_name)) except AccessError: raise DataJointError( - "Schema `{name}` does not exist and could not be created. " - "Check permissions.".format(name=schema_name) + "Schema `{name}` does not exist and could not be created. " "Check permissions.".format(name=schema_name) ) else: self.log("created") @@ -156,10 +148,7 @@ def activate( def _assert_exists(self, message=None): if not self.exists: - raise DataJointError( - message - or "Schema `{db}` has not been created.".format(db=self.database) - ) + raise DataJointError(message or "Schema `{db}` has not been created.".format(db=self.database)) def __call__(self, cls, *, context=None): """ @@ -170,9 +159,7 @@ def __call__(self, cls, *, context=None): """ context = context or self.context or inspect.currentframe().f_back.f_locals if issubclass(cls, Part): - raise DataJointError( - "The schema decorator should not be applied to Part tables." - ) + raise DataJointError("The schema decorator should not be applied to Part tables.") if self.is_activated(): self._decorate_master(cls, context) else: @@ -185,9 +172,7 @@ def _decorate_master(self, cls, context): :param cls: the master class to process :param context: the class' declaration context """ - self._decorate_table( - cls, context=dict(context, self=cls, **{cls.__name__: cls}) - ) + self._decorate_table(cls, context=dict(context, self=cls, **{cls.__name__: cls})) # Process part tables for part in ordered_dir(cls): if part[0].isupper(): @@ -197,9 +182,7 @@ def _decorate_master(self, cls, context): # allow addressing master by name or keyword 'master' self._decorate_table( part, - context=dict( - context, master=cls, self=part, **{cls.__name__: cls} - ), + context=dict(context, master=cls, self=part, **{cls.__name__: cls}), ) def _decorate_table(self, table_class, context, assert_declared=False): @@ -229,26 +212,17 @@ def _decorate_table(self, table_class, context, assert_declared=False): # add table definition to the doc string if isinstance(table_class.definition, str): - table_class.__doc__ = ( - (table_class.__doc__ or "") - + "\nTable definition:\n\n" - + table_class.definition - ) + table_class.__doc__ = (table_class.__doc__ or "") + "\nTable definition:\n\n" + table_class.definition # fill values in Lookup tables from their contents property - if ( - isinstance(instance, Lookup) - and hasattr(instance, "contents") - and is_declared - ): + if isinstance(instance, Lookup) and hasattr(instance, "contents") and is_declared: contents = list(instance.contents) if len(contents) > len(instance): if instance.heading.has_autoincrement: warnings.warn( - ( - "Contents has changed but cannot be inserted because " - "{table} has autoincrement." - ).format(table=instance.__class__.__name__) + ("Contents has changed but cannot be inserted because " "{table} has autoincrement.").format( + table=instance.__class__.__name__ + ) ) else: instance.insert(contents, skip_duplicates=True) @@ -274,9 +248,7 @@ def size_on_disk(self): """ SELECT SUM(data_length + index_length) FROM information_schema.tables WHERE table_schema='{db}' - """.format( - db=self.database - ) + """.format(db=self.database) ).fetchone()[0] ) @@ -299,10 +271,7 @@ def spawn_missing_classes(self, context=None): tables = [ row[0] for row in self.connection.query("SHOW TABLES in `%s`" % self.database) - if lookup_class_name( - "`{db}`.`{tab}`".format(db=self.database, tab=row[0]), context, 0 - ) - is None + if lookup_class_name("`{db}`.`{tab}`".format(db=self.database, tab=row[0]), context, 0) is None ] master_classes = (Lookup, Manual, Imported, Computed) part_tables = [] @@ -310,19 +279,13 @@ def spawn_missing_classes(self, context=None): class_name = to_camel_case(table_name) if class_name not in context: try: - cls = next( - cls - for cls in master_classes - if re.fullmatch(cls.tier_regexp, table_name) - ) + cls = next(cls for cls in master_classes if re.fullmatch(cls.tier_regexp, table_name)) except StopIteration: if re.fullmatch(Part.tier_regexp, table_name): part_tables.append(table_name) else: # declare and decorate master table classes - context[class_name] = self( - type(class_name, (cls,), dict()), context=context - ) + context[class_name] = self(type(class_name, (cls,), dict()), context=context) # attach parts to masters for table_name in part_tables: @@ -331,10 +294,7 @@ def spawn_missing_classes(self, context=None): try: master_class = context[to_camel_case(groups["master"])] except KeyError: - raise DataJointError( - "The table %s does not follow DataJoint naming conventions" - % table_name - ) + raise DataJointError("The table %s does not follow DataJoint naming conventions" % table_name) part_class = type(class_name, (Part,), dict(definition=...)) part_class._master = master_class self._decorate_table(part_class, context=context, assert_declared=True) @@ -345,33 +305,19 @@ def drop(self, force=False): Drop the associated schema if it exists """ if not self.exists: - logger.info( - "Schema named `{database}` does not exist. Doing nothing.".format( - database=self.database - ) - ) + logger.info("Schema named `{database}` does not exist. Doing nothing.".format(database=self.database)) elif ( not config["safemode"] or force - or user_choice( - "Proceed to delete entire schema `%s`?" % self.database, default="no" - ) - == "yes" + or user_choice("Proceed to delete entire schema `%s`?" % self.database, default="no") == "yes" ): logger.debug("Dropping `{database}`.".format(database=self.database)) try: - self.connection.query( - "DROP DATABASE `{database}`".format(database=self.database) - ) - logger.debug( - "Schema `{database}` was dropped successfully.".format( - database=self.database - ) - ) + self.connection.query("DROP DATABASE `{database}`".format(database=self.database)) + logger.debug("Schema `{database}` was dropped successfully.".format(database=self.database)) except AccessError: raise AccessError( - "An attempt to drop schema `{database}` " - "has failed. Check permissions.".format(database=self.database) + "An attempt to drop schema `{database}` " "has failed. Check permissions.".format(database=self.database) ) @property @@ -383,9 +329,9 @@ def exists(self): raise DataJointError("Schema must be activated first.") return bool( self.connection.query( - "SELECT schema_name " - "FROM information_schema.schemata " - "WHERE schema_name = '{database}'".format(database=self.database) + "SELECT schema_name " "FROM information_schema.schemata " "WHERE schema_name = '{database}'".format( + database=self.database + ) ).rowcount ) @@ -417,9 +363,7 @@ def save(self, python_filename=None): self._assert_exists() module_count = itertools.count() # add virtual modules for referenced modules with names vmod0, vmod1, ... - module_lookup = collections.defaultdict( - lambda: "vmod" + str(next(module_count)) - ) + module_lookup = collections.defaultdict(lambda: "vmod" + str(next(module_count))) db = self.database def make_class_definition(table): @@ -438,9 +382,7 @@ def replace(s): ) return ("" if tier == "Part" else "\n@schema\n") + ( - "{indent}class {class_name}(dj.{tier}):\n" - '{indent} definition = """\n' - '{indent} {defi}"""' + "{indent}class {class_name}(dj.{tier}):\n" '{indent} definition = """\n' '{indent} {defi}"""' ).format( class_name=class_name, indent=indent, @@ -459,9 +401,7 @@ def replace(s): '"""This module was auto-generated by datajoint from an existing schema"""', "import datajoint as dj\n\nschema = dj.Schema('{db}')".format(db=db), "\n".join( - "{module} = dj.VirtualModule('{module}', '{schema_name}')".format( - module=v, schema_name=k - ) + "{module} = dj.VirtualModule('{module}', '{schema_name}')".format(module=v, schema_name=k) for k, v in module_lookup.items() ), body, @@ -482,10 +422,7 @@ def list_tables(self): self.connection.dependencies.load() return [ t - for d, t in ( - table_name.replace("`", "").split(".") - for table_name in self.connection.dependencies.topo_sort() - ) + for d, t in (table_name.replace("`", "").split(".") for table_name in self.connection.dependencies.topo_sort()) if d == self.database ] @@ -539,8 +476,6 @@ def list_schemas(connection=None): return [ r[0] for r in (connection or conn()).query( - "SELECT schema_name " - "FROM information_schema.schemata " - 'WHERE schema_name <> "information_schema"' + "SELECT schema_name " "FROM information_schema.schemata " 'WHERE schema_name <> "information_schema"' ) ] diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 30b206f99..3e16c0484 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -141,9 +141,7 @@ def get_store_spec(self, store): try: spec = self["stores"][store] except KeyError: - raise DataJointError( - "Storage {store} is requested but not configured".format(store=store) - ) + raise DataJointError("Storage {store} is requested but not configured".format(store=store)) spec["subfolding"] = spec.get("subfolding", DEFAULT_SUBFOLDING) spec_keys = { # REQUIRED in uppercase and allowed in lowercase @@ -165,22 +163,14 @@ def get_store_spec(self, store): try: spec_keys = spec_keys[spec.get("protocol", "").lower()] except KeyError: - raise DataJointError( - 'Missing or invalid protocol in dj.config["stores"]["{store}"]'.format( - store=store - ) - ) + raise DataJointError('Missing or invalid protocol in dj.config["stores"]["{store}"]'.format(store=store)) # check that all required keys are present in spec try: raise DataJointError( 'dj.config["stores"]["{store}"] is missing "{k}"'.format( store=store, - k=next( - k.lower() - for k in spec_keys - if k.isupper() and k.lower() not in spec - ), + k=next(k.lower() for k in spec_keys if k.isupper() and k.lower() not in spec), ) ) except StopIteration: @@ -191,11 +181,7 @@ def get_store_spec(self, store): raise DataJointError( 'Invalid key "{k}" in dj.config["stores"]["{store}"]'.format( store=store, - k=next( - k - for k in spec - if k.upper() not in spec_keys and k.lower() not in spec_keys - ), + k=next(k for k in spec if k.upper() not in spec_keys and k.lower() not in spec_keys), ) ) except StopIteration: @@ -254,17 +240,13 @@ def __setitem__(self, key, value): valid_logging_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} if key == "loglevel": if value not in valid_logging_levels: - raise ValueError( - f"'{value}' is not a valid logging value {tuple(valid_logging_levels)}" - ) + raise ValueError(f"'{value}' is not a valid logging value {tuple(valid_logging_levels)}") logger.setLevel(value) # Load configuration from file config = Config() -config_files = ( - os.path.expanduser(n) for n in (LOCALCONFIG, os.path.join("~", GLOBALCONFIG)) -) +config_files = (os.path.expanduser(n) for n in (LOCALCONFIG, os.path.join("~", GLOBALCONFIG))) try: config.load(next(n for n in config_files if os.path.exists(n))) except StopIteration: diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 7e3e0c3a1..94b140797 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -83,9 +83,7 @@ def class_name(self): @property def definition(self): - raise NotImplementedError( - "Subclasses of Table must implement the `definition` property" - ) + raise NotImplementedError("Subclasses of Table must implement the `definition` property") def declare(self, context=None): """ @@ -95,16 +93,11 @@ def declare(self, context=None): not allowed. """ if self.connection.in_transaction: - raise DataJointError( - "Cannot declare new tables inside a transaction, " - "e.g. from inside a populate/make call" - ) + raise DataJointError("Cannot declare new tables inside a transaction, " "e.g. from inside a populate/make call") # Enforce strict CamelCase #1150 if not is_camel_case(self.class_name): raise DataJointError( - "Table class name `{name}` is invalid. Please use CamelCase. ".format( - name=self.class_name - ) + "Table class name `{name}` is invalid. Please use CamelCase. ".format(name=self.class_name) + "Classes defining tables should be formatted in strict CamelCase." ) sql, external_stores = declare(self.full_table_name, self.definition, context) @@ -126,8 +119,7 @@ def alter(self, prompt=True, context=None): """ if self.connection.in_transaction: raise DataJointError( - "Cannot update table declaration inside a transaction, " - "e.g. from inside a populate/make call" + "Cannot update table declaration inside a transaction, " "e.g. from inside a populate/make call" ) if context is None: frame = inspect.currentframe().f_back @@ -139,9 +131,7 @@ def alter(self, prompt=True, context=None): if prompt: logger.warning("Nothing to alter.") else: - sql = "ALTER TABLE {tab}\n\t".format( - tab=self.full_table_name - ) + ",\n\t".join(sql) + sql = "ALTER TABLE {tab}\n\t".format(tab=self.full_table_name) + ",\n\t".join(sql) if not prompt or user_choice(sql + "\n\nExecute?") == "yes": try: # declare all external tables before declaring main table @@ -153,9 +143,7 @@ def alter(self, prompt=True, context=None): pass else: # reset heading - self.__class__._heading = Heading( - table_info=self.heading.table_info - ) + self.__class__._heading = Heading(table_info=self.heading.table_info) if prompt: logger.info("Table altered") self._log("Altered " + self.full_table_name) @@ -170,9 +158,7 @@ def get_select_fields(self, select_fields=None): """ :return: the selected attributes from the SQL SELECT statement. """ - return ( - "*" if select_fields is None else self.heading.project(select_fields).as_sql - ) + return "*" if select_fields is None else self.heading.project(select_fields).as_sql def parents(self, primary=None, as_objects=False, foreign_key_info=False): """ @@ -260,9 +246,7 @@ def is_declared(self): """ return ( self.connection.query( - 'SHOW TABLES in `{database}` LIKE "{table_name}"'.format( - database=self.database, table_name=self.table_name - ) + 'SHOW TABLES in `{database}` LIKE "{table_name}"'.format(database=self.database, table_name=self.table_name) ).rowcount > 0 ) @@ -311,14 +295,9 @@ def update1(self, row): if not isinstance(row, collections.abc.Mapping): raise DataJointError("The argument of update1 must be dict-like.") if not set(row).issuperset(self.primary_key): - raise DataJointError( - "The argument of update1 must supply all primary key values." - ) + raise DataJointError("The argument of update1 must supply all primary key values.") try: - raise DataJointError( - "Attribute `%s` not found." - % next(k for k in row if k not in self.heading.names) - ) + raise DataJointError("Attribute `%s` not found." % next(k for k in row if k not in self.heading.names)) except StopIteration: pass # ok if len(self.restriction): @@ -327,11 +306,7 @@ def update1(self, row): if len(self & key) != 1: raise DataJointError("Update can only be applied to one existing entry.") # UPDATE query - row = [ - self.__make_placeholder(k, v) - for k, v in row.items() - if k not in self.primary_key - ] + row = [self.__make_placeholder(k, v) for k, v in row.items() if k not in self.primary_key] query = "UPDATE {table} SET {assignments} WHERE {where}".format( table=self.full_table_name, assignments=",".join("`%s`=%s" % r[:2] for r in row), @@ -379,9 +354,7 @@ def insert( if isinstance(rows, pandas.DataFrame): # drop 'extra' synthetic index for 1-field index case - # frames with more advanced indices should be prepared by user. - rows = rows.reset_index( - drop=len(rows.index.names) == 1 and not rows.index.names[0] - ).to_records(index=False) + rows = rows.reset_index(drop=len(rows.index.names) == 1 and not rows.index.names[0]).to_records(index=False) if isinstance(rows, Path): with open(rows, newline="") as data_file: @@ -403,10 +376,7 @@ def insert( try: raise DataJointError( "Attribute %s not found. To ignore extra attributes in insert, " - "set ignore_extra_fields=True." - % next( - name for name in rows.heading if name not in self.heading - ) + "set ignore_extra_fields=True." % next(name for name in rows.heading if name not in self.heading) ) except StopIteration: pass @@ -417,9 +387,7 @@ def insert( table=self.full_table_name, select=rows.make_sql(fields), duplicate=( - " ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`".format( - table=self.full_table_name, pk=self.primary_key[0] - ) + " ON DUPLICATE KEY UPDATE `{pk}`={table}.`{pk}`".format(table=self.full_table_name, pk=self.primary_key[0]) if skip_duplicates else "" ), @@ -429,43 +397,26 @@ def insert( # collects the field list from first row (passed by reference) field_list = [] - rows = list( - self.__make_row_to_insert(row, field_list, ignore_extra_fields) - for row in rows - ) + rows = list(self.__make_row_to_insert(row, field_list, ignore_extra_fields) for row in rows) if rows: try: query = "{command} INTO {destination}(`{fields}`) VALUES {placeholders}{duplicate}".format( command="REPLACE" if replace else "INSERT", destination=self.from_clause(), fields="`,`".join(field_list), - placeholders=",".join( - "(" + ",".join(row["placeholders"]) + ")" for row in rows - ), + placeholders=",".join("(" + ",".join(row["placeholders"]) + ")" for row in rows), duplicate=( - " ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`".format( - pk=self.primary_key[0] - ) - if skip_duplicates - else "" + " ON DUPLICATE KEY UPDATE `{pk}`=`{pk}`".format(pk=self.primary_key[0]) if skip_duplicates else "" ), ) self.connection.query( query, - args=list( - itertools.chain.from_iterable( - (v for v in r["values"] if v is not None) for r in rows - ) - ), + args=list(itertools.chain.from_iterable((v for v in r["values"] if v is not None) for r in rows)), ) except UnknownAttributeError as err: - raise err.suggest( - "To ignore extra fields in insert, set ignore_extra_fields=True" - ) + raise err.suggest("To ignore extra fields in insert, set ignore_extra_fields=True") except DuplicateError as err: - raise err.suggest( - "To ignore duplicate entries in insert, set skip_duplicates=True" - ) + raise err.suggest("To ignore duplicate entries in insert, set skip_duplicates=True") def delete_quick(self, get_count=False): """ @@ -474,11 +425,7 @@ def delete_quick(self, get_count=False): """ query = "DELETE FROM " + self.full_table_name + self.where_clause() self.connection.query(query) - count = ( - self.connection.query("SELECT ROW_COUNT()").fetchone()[0] - if get_count - else None - ) + count = self.connection.query("SELECT ROW_COUNT()").fetchone()[0] if get_count else None self._log(query[:255]) return count @@ -529,18 +476,10 @@ def cascade(table): match = match.groupdict() # if schema name missing, use table if "`.`" not in match["child"]: - match["child"] = "{}.{}".format( - table.full_table_name.split(".")[0], match["child"] - ) - if ( - match["pk_attrs"] is not None - ): # fully matched, adjusting the keys - match["fk_attrs"] = [ - k.strip("`") for k in match["fk_attrs"].split(",") - ] - match["pk_attrs"] = [ - k.strip("`") for k in match["pk_attrs"].split(",") - ] + match["child"] = "{}.{}".format(table.full_table_name.split(".")[0], match["child"]) + if match["pk_attrs"] is not None: # fully matched, adjusting the keys + match["fk_attrs"] = [k.strip("`") for k in match["fk_attrs"].split(",")] + match["pk_attrs"] = [k.strip("`") for k in match["pk_attrs"].split(",")] else: # only partially matched, querying with constraint to determine keys match["fk_attrs"], match["parent"], match["pk_attrs"] = list( map( @@ -550,10 +489,7 @@ def cascade(table): constraint_info_query, args=( match["name"].strip("`"), - *[ - _.strip("`") - for _ in match["child"].split("`.`") - ], + *[_.strip("`") for _ in match["child"].split("`.`")], ), ).fetchall() ), @@ -566,16 +502,11 @@ def cascade(table): # 2. if child renames any attributes # Otherwise restrict child by table's restriction. child = FreeTable(table.connection, match["child"]) - if ( - set(table.restriction_attributes) <= set(child.primary_key) - and match["fk_attrs"] == match["pk_attrs"] - ): + if set(table.restriction_attributes) <= set(child.primary_key) and match["fk_attrs"] == match["pk_attrs"]: child._restriction = table._restriction child._restriction_attributes = table.restriction_attributes elif match["fk_attrs"] != match["pk_attrs"]: - child &= table.proj( - **dict(zip(match["fk_attrs"], match["pk_attrs"])) - ) + child &= table.proj(**dict(zip(match["fk_attrs"], match["pk_attrs"]))) else: child &= table.proj() @@ -601,11 +532,7 @@ def cascade(table): cascade(child) else: deleted.add(table.full_table_name) - logger.info( - "Deleting {count} rows from {table}".format( - count=delete_count, table=table.full_table_name - ) - ) + logger.info("Deleting {count} rows from {table}".format(count=delete_count, table=table.full_table_name)) break else: raise DataJointError("Exceeded maximum number of delete attempts.") @@ -642,8 +569,9 @@ def cascade(table): if transaction: self.connection.cancel_transaction() raise DataJointError( - "Attempt to delete part table {part} before deleting from " - "its master {master} first.".format(part=part, master=master) + "Attempt to delete part table {part} before deleting from " "its master {master} first.".format( + part=part, master=master + ) ) # Confirm and commit @@ -677,9 +605,7 @@ def drop_quick(self): logger.info("Dropped table %s" % self.full_table_name) self._log(query[:255]) else: - logger.info( - "Nothing to drop: table %s is not declared" % self.full_table_name - ) + logger.info("Nothing to drop: table %s is not declared" % self.full_table_name) def drop(self): """ @@ -688,31 +614,25 @@ def drop(self): """ if self.restriction: raise DataJointError( - "A table with an applied restriction cannot be dropped." - " Call drop() on the unrestricted Table." + "A table with an applied restriction cannot be dropped." " Call drop() on the unrestricted Table." ) self.connection.dependencies.load() do_drop = True - tables = [ - table - for table in self.connection.dependencies.descendants(self.full_table_name) - if not table.isdigit() - ] + tables = [table for table in self.connection.dependencies.descendants(self.full_table_name) if not table.isdigit()] # avoid dropping part tables without their masters: See issue #374 for part in tables: master = get_master(part) if master and master not in tables: raise DataJointError( - "Attempt to drop part table {part} before dropping " - "its master. Drop {master} first.".format(part=part, master=master) + "Attempt to drop part table {part} before dropping " "its master. Drop {master} first.".format( + part=part, master=master + ) ) if config["safemode"]: for table in tables: - logger.info( - table + " (%d tuples)" % len(FreeTable(self.connection, table)) - ) + logger.info(table + " (%d tuples)" % len(FreeTable(self.connection, table))) do_drop = user_choice("Proceed?", default="no") == "yes" if do_drop: for table in reversed(tables): @@ -725,9 +645,7 @@ def size_on_disk(self): :return: size of data and indices in bytes on the storage device """ ret = self.connection.query( - 'SHOW TABLE STATUS FROM `{database}` WHERE NAME="{table}"'.format( - database=self.database, table=self.table_name - ), + 'SHOW TABLE STATUS FROM `{database}` WHERE NAME="{table}"'.format(database=self.database, table=self.table_name), as_dict=True, ).fetchone() return ret["Data_length"] + ret["Index_length"] @@ -744,11 +662,7 @@ def describe(self, context=None, printout=False): self.connection.dependencies.load() parents = self.parents(foreign_key_info=True) in_key = True - definition = ( - "# " + self.heading.table_status["comment"] + "\n" - if self.heading.table_status["comment"] - else "" - ) + definition = "# " + self.heading.table_status["comment"] + "\n" if self.heading.table_status["comment"] else "" attributes_thus_far = set() attributes_declared = set() indexes = self.heading.indexes.copy() @@ -769,51 +683,34 @@ def describe(self, context=None, printout=False): index_props = "" else: index_props = [k for k, v in index_props.items() if v] - index_props = ( - " [{}]".format(", ".join(index_props)) - if index_props - else "" - ) + index_props = " [{}]".format(", ".join(index_props)) if index_props else "" if not fk_props["aliased"]: # simple foreign key definition += "->{props} {class_name}\n".format( props=index_props, - class_name=lookup_class_name(parent_name, context) - or parent_name, + class_name=lookup_class_name(parent_name, context) or parent_name, ) else: # projected foreign key - definition += ( - "->{props} {class_name}.proj({proj_list})\n".format( - props=index_props, - class_name=lookup_class_name(parent_name, context) - or parent_name, - proj_list=",".join( - '{}="{}"'.format(attr, ref) - for attr, ref in fk_props["attr_map"].items() - if ref != attr - ), - ) + definition += "->{props} {class_name}.proj({proj_list})\n".format( + props=index_props, + class_name=lookup_class_name(parent_name, context) or parent_name, + proj_list=",".join( + '{}="{}"'.format(attr, ref) for attr, ref in fk_props["attr_map"].items() if ref != attr + ), ) attributes_declared.update(fk_props["attr_map"]) if do_include: attributes_declared.add(attr.name) definition += "%-20s : %-28s %s\n" % ( - ( - attr.name - if attr.default is None - else "%s=%s" % (attr.name, attr.default) - ), - "%s%s" - % (attr.type, " auto_increment" if attr.autoincrement else ""), + (attr.name if attr.default is None else "%s=%s" % (attr.name, attr.default)), + "%s%s" % (attr.type, " auto_increment" if attr.autoincrement else ""), "# " + attr.comment if attr.comment else "", ) # add remaining indexes for k, v in indexes.items(): - definition += "{unique}INDEX ({attrs})\n".format( - unique="UNIQUE " if v["unique"] else "", attrs=", ".join(k) - ) + definition += "{unique}INDEX ({attrs})\n".format(unique="UNIQUE " if v["unique"] else "", attrs=", ".join(k)) if printout: logger.info("\n" + definition) return definition @@ -843,35 +740,19 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False): try: value = uuid.UUID(value) except (AttributeError, ValueError): - raise DataJointError( - "badly formed UUID value {v} for attribute `{n}`".format( - v=value, n=name - ) - ) + raise DataJointError("badly formed UUID value {v} for attribute `{n}`".format(v=value, n=name)) value = value.bytes elif attr.is_blob: value = blob.pack(value) - value = ( - self.external[attr.store].put(value).bytes - if attr.is_external - else value - ) + value = self.external[attr.store].put(value).bytes if attr.is_external else value elif attr.is_attachment: attachment_path = Path(value) if attr.is_external: # value is hash of contents - value = ( - self.external[attr.store] - .upload_attachment(attachment_path) - .bytes - ) + value = self.external[attr.store].upload_attachment(attachment_path).bytes else: # value is filename + contents - value = ( - str.encode(attachment_path.name) - + b"\0" - + attachment_path.read_bytes() - ) + value = str.encode(attachment_path.name) + b"\0" + attachment_path.read_bytes() elif attr.is_filepath: value = self.external[attr.store].upload_filepath(value).bytes elif attr.numeric: @@ -898,9 +779,7 @@ def check_fields(fields): if not ignore_extra_fields: for field in fields: if field not in self.heading: - raise KeyError( - "`{0:s}` is not in the table heading".format(field) - ) + raise KeyError("`{0:s}` is not in the table heading".format(field)) elif set(field_list) != set(fields).intersection(self.heading.names): raise DataJointError("Attempt to insert rows with different fields.") @@ -914,25 +793,20 @@ def check_fields(fields): elif isinstance(row, collections.abc.Mapping): # dict-based check_fields(row) attributes = [ - self.__make_placeholder(name, row[name], ignore_extra_fields) - for name in self.heading - if name in row + self.__make_placeholder(name, row[name], ignore_extra_fields) for name in self.heading if name in row ] else: # positional try: if len(row) != len(self.heading): raise DataJointError( "Invalid insert argument. Incorrect number of attributes: " - "{given} given; {expected} expected".format( - given=len(row), expected=len(self.heading) - ) + "{given} given; {expected} expected".format(given=len(row), expected=len(self.heading)) ) except TypeError: raise DataJointError("Datatype %s cannot be inserted" % type(row)) else: attributes = [ - self.__make_placeholder(name, value, ignore_extra_fields) - for name, value in zip(self.heading, row) + self.__make_placeholder(name, value, ignore_extra_fields) for name, value in zip(self.heading, row) ] if ignore_extra_fields: attributes = [a for a in attributes if a is not None] @@ -946,9 +820,7 @@ def check_fields(fields): # reorder attributes in row_to_insert to match field_list order = list(row_to_insert["names"].index(field) for field in field_list) row_to_insert["names"] = list(row_to_insert["names"][i] for i in order) - row_to_insert["placeholders"] = list( - row_to_insert["placeholders"][i] for i in order - ) + row_to_insert["placeholders"] = list(row_to_insert["placeholders"][i] for i in order) row_to_insert["values"] = list(row_to_insert["values"][i] for i in order) return row_to_insert @@ -977,24 +849,10 @@ def lookup_class_name(name, context, depth=3): except AttributeError: pass # not a UserTable -- cannot have part tables. else: - for part in ( - getattr(member, p) - for p in parts - if p[0].isupper() and hasattr(member, p) - ): - if ( - inspect.isclass(part) - and issubclass(part, Table) - and part.full_table_name == name - ): - return ".".join( - [node["context_name"], member_name, part.__name__] - ).lstrip(".") - elif ( - node["depth"] > 0 - and inspect.ismodule(member) - and member.__name__ != "datajoint" - ): + for part in (getattr(member, p) for p in parts if p[0].isupper() and hasattr(member, p)): + if inspect.isclass(part) and issubclass(part, Table) and part.full_table_name == name: + return ".".join([node["context_name"], member_name, part.__name__]).lstrip(".") + elif node["depth"] > 0 and inspect.ismodule(member) and member.__name__ != "datajoint": try: nodes.append( dict( @@ -1018,9 +876,7 @@ class FreeTable(Table): """ def __init__(self, conn, full_table_name): - self.database, self._table_name = ( - s.strip("`") for s in full_table_name.split(".") - ) + self.database, self._table_name = (s.strip("`") for s in full_table_name.split(".")) self._connection = conn self._support = [full_table_name] self._heading = Heading( @@ -1033,10 +889,7 @@ def __init__(self, conn, full_table_name): ) def __repr__(self): - return ( - "FreeTable(`%s`.`%s`)\n" % (self.database, self._table_name) - + super().__repr__() - ) + return "FreeTable(`%s`.`%s`)\n" % (self.database, self._table_name) + super().__repr__() class Log(Table): @@ -1053,11 +906,7 @@ def __init__(self, conn, database, skip_logging=False): self.database = database self.skip_logging = skip_logging self._connection = conn - self._heading = Heading( - table_info=dict( - conn=conn, database=database, table_name=self.table_name, context=None - ) - ) + self._heading = Heading(table_info=dict(conn=conn, database=database, table_name=self.table_name, context=None)) self._support = [self.full_table_name] self._definition = """ # event logging table for `{database}` @@ -1068,9 +917,7 @@ def __init__(self, conn, database, skip_logging=False): user :varchar(255) # user@host host="" :varchar(255) # system hostname event="" :varchar(255) # event message - """.format( - database=database - ) + """.format(database=database) super().__init__() diff --git a/src/datajoint/user_tables.py b/src/datajoint/user_tables.py index 9c2e79d34..d7faeb285 100644 --- a/src/datajoint/user_tables.py +++ b/src/datajoint/user_tables.py @@ -52,11 +52,7 @@ class TableMeta(type): def __getattribute__(cls, name): # trigger instantiation for supported class attrs - return ( - cls().__getattribute__(name) - if name in supported_class_attrs - else super().__getattribute__(name) - ) + return cls().__getattribute__(name) if name in supported_class_attrs else super().__getattribute__(name) def __and__(cls, arg): return cls() & arg @@ -103,9 +99,7 @@ def definition(self): """ :return: a string containing the table definition using the DataJoint DDL. """ - raise NotImplementedError( - 'Subclasses of Table must implement the property "definition"' - ) + raise NotImplementedError('Subclasses of Table must implement the property "definition"') @ClassProperty def connection(cls): @@ -125,10 +119,7 @@ def full_table_name(cls): if cls not in {Manual, Imported, Lookup, Computed, Part, UserTable}: # for derived classes only if cls.database is None: - raise DataJointError( - "Class %s is not properly declared (schema decorator not applied?)" - % cls.__name__ - ) + raise DataJointError("Class %s is not properly declared (schema decorator not applied?)" % cls.__name__) return r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name) @@ -149,9 +140,7 @@ class Lookup(UserTable): """ _prefix = "#" - tier_regexp = ( - r"(?P" + _prefix + _base_regexp.replace("TIER", "lookup") + ")" - ) + tier_regexp = r"(?P" + _prefix + _base_regexp.replace("TIER", "lookup") + ")" class Imported(UserTable, AutoPopulate): @@ -202,9 +191,7 @@ def connection(cls): @ClassProperty def full_table_name(cls): return ( - None - if cls.database is None or cls.table_name is None - else r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name) + None if cls.database is None or cls.table_name is None else r"`{0:s}`.`{1:s}`".format(cls.database, cls.table_name) ) @ClassProperty @@ -213,11 +200,7 @@ def master(cls): @ClassProperty def table_name(cls): - return ( - None - if cls.master is None - else cls.master.table_name + "__" + from_camel_case(cls.__name__) - ) + return None if cls.master is None else cls.master.table_name + "__" + from_camel_case(cls.__name__) def delete(self, force=False): """ @@ -226,9 +209,7 @@ def delete(self, force=False): if force: super().delete(force_parts=True) else: - raise DataJointError( - "Cannot delete from a Part directly. Delete from master instead" - ) + raise DataJointError("Cannot delete from a Part directly. Delete from master instead") def drop(self, force=False): """ @@ -237,9 +218,7 @@ def drop(self, force=False): if force: super().drop() else: - raise DataJointError( - "Cannot drop a Part directly. Delete from master instead" - ) + raise DataJointError("Cannot drop a Part directly. Delete from master instead") def alter(self, prompt=True, context=None): # without context, use declaration context which maps master keyword to master table @@ -263,10 +242,6 @@ def _get_tier(table_name): return _AliasNode else: try: - return next( - tier - for tier in user_table_classes - if re.fullmatch(tier.tier_regexp, table_name.split("`")[-2]) - ) + return next(tier for tier in user_table_classes if re.fullmatch(tier.tier_regexp, table_name.split("`")[-2])) except StopIteration: return None diff --git a/src/datajoint/utils.py b/src/datajoint/utils.py index c34536685..16927965e 100644 --- a/src/datajoint/utils.py +++ b/src/datajoint/utils.py @@ -25,9 +25,7 @@ def user_choice(prompt, choices=("yes", "no"), default=None): :return: the user's choice """ assert default is None or default in choices - choice_list = ", ".join( - (choice.title() if choice == default else choice for choice in choices) - ) + choice_list = ", ".join((choice.title() if choice == default else choice for choice in choices)) response = None while response not in choices: response = input(prompt + " [" + choice_list + "]: ") @@ -97,9 +95,7 @@ def convert(match): return ("_" if match.groups()[0] else "") + match.group(0).lower() if not is_camel_case(s): - raise DataJointError( - "ClassName must be alphanumeric in CamelCase, begin with a capital letter" - ) + raise DataJointError("ClassName must be alphanumeric in CamelCase, begin with a capital letter") return re.sub(r"(\B[A-Z])|(\b[A-Z])", convert, s) diff --git a/tests/conftest.py b/tests/conftest.py index 88d55e32f..db48ecfe4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,26 +1,21 @@ -import json import os -import shutil -from os import environ, remove -from pathlib import Path +from os import remove from typing import Dict, List import certifi import minio -import networkx as nx import pytest import urllib3 from packaging import version import datajoint as dj -from datajoint import errors from datajoint.errors import ( ADAPTED_TYPE_SWITCH, FILEPATH_FEATURE_SWITCH, DataJointError, ) -from . import schema, schema_adapted, schema_advanced, schema_external, schema_simple +from . import schema, schema_advanced, schema_external, schema_simple from . import schema_uuid as schema_uuid_module @@ -85,9 +80,7 @@ def connection_root(connection_root_bare, prefix): dj.config["safemode"] = False conn_root = connection_root_bare # Create MySQL users - if version.parse( - conn_root.query("select @@version;").fetchone()[0] - ) >= version.parse("8.0.0"): + if version.parse(conn_root.query("select @@version;").fetchone()[0]) >= version.parse("8.0.0"): # create user if necessary on mysql8 conn_root.query( """ @@ -120,9 +113,7 @@ def connection_root(connection_root_bare, prefix): IDENTIFIED BY 'datajoint'; """ ) - conn_root.query( - "GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%' IDENTIFIED BY 'djview';" - ) + conn_root.query("GRANT SELECT ON `djtest%%`.* TO 'djview'@'%%' IDENTIFIED BY 'djview';") conn_root.query( """ GRANT SELECT ON `djtest%%`.* TO 'djssl'@'%%' @@ -156,9 +147,7 @@ def connection_test(connection_root, prefix, db_creds_test): permission = "ALL PRIVILEGES" # Create MySQL users - if version.parse( - connection_root.query("select @@version;").fetchone()[0] - ) >= version.parse("8.0.0"): + if version.parse(connection_root.query("select @@version;").fetchone()[0]) >= version.parse("8.0.0"): # create user if necessary on mysql8 connection_root.query( f""" @@ -214,12 +203,8 @@ def stores_config(s3_creds, tmpdir_factory): location="dj/repo", stage=tmpdir_factory.mktemp("repo-s3"), ), - "local": dict( - protocol="file", location=tmpdir_factory.mktemp("local"), subfolding=(1, 1) - ), - "share": dict( - s3_creds, protocol="s3", location="dj/store/repo", subfolding=(2, 4) - ), + "local": dict(protocol="file", location=tmpdir_factory.mktemp("local"), subfolding=(1, 1)), + "share": dict(s3_creds, protocol="s3", location="dj/store/repo", subfolding=(2, 4)), } return stores_config @@ -248,9 +233,7 @@ def mock_cache(tmpdir_factory): @pytest.fixture def schema_any(connection_test, prefix): - schema_any = dj.Schema( - prefix + "_test1", schema.LOCALS_ANY, connection=connection_test - ) + schema_any = dj.Schema(prefix + "_test1", schema.LOCALS_ANY, connection=connection_test) assert schema.LOCALS_ANY, "LOCALS_ANY is empty" try: schema_any.jobs.delete() @@ -324,9 +307,7 @@ def thing_tables(schema_any): @pytest.fixture def schema_simp(connection_test, prefix): - schema = dj.Schema( - prefix + "_relational", schema_simple.LOCALS_SIMPLE, connection=connection_test - ) + schema = dj.Schema(prefix + "_relational", schema_simple.LOCALS_SIMPLE, connection=connection_test) schema(schema_simple.SelectPK) schema(schema_simple.KeyPK) schema(schema_simple.IJ) @@ -373,9 +354,7 @@ def schema_adv(connection_test, prefix): @pytest.fixture -def schema_ext( - connection_test, enable_filepath_feature, mock_stores, mock_cache, prefix -): +def schema_ext(connection_test, enable_filepath_feature, mock_stores, mock_cache, prefix): schema = dj.Schema( prefix + "_extern", context=schema_external.LOCALS_EXTERNAL, @@ -414,9 +393,7 @@ def http_client(): timeout=30, cert_reqs="CERT_REQUIRED", ca_certs=certifi.where(), - retries=urllib3.Retry( - total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504] - ), + retries=urllib3.Retry(total=3, backoff_factor=0.2, status_forcelist=[500, 502, 503, 504]), ) yield client @@ -450,12 +427,7 @@ def minio_client(s3_creds, minio_client_bare, teardown=False): # Teardown S3 objs = list(minio_client_bare.list_objects(s3_creds["bucket"], recursive=True)) - objs = [ - minio_client_bare.remove_object( - s3_creds["bucket"], o.object_name.encode("utf-8") - ) - for o in objs - ] + objs = [minio_client_bare.remove_object(s3_creds["bucket"], o.object_name.encode("utf-8")) for o in objs] minio_client_bare.remove_bucket(s3_creds["bucket"]) diff --git a/tests/schema.py b/tests/schema.py index 2b7977465..7abb08a4d 100644 --- a/tests/schema.py +++ b/tests/schema.py @@ -154,9 +154,7 @@ def make(self, key): dict( key, experiment_id=experiment_id, - experiment_date=( - date.today() - timedelta(random.expovariate(1 / 30)) - ).isoformat(), + experiment_date=(date.today() - timedelta(random.expovariate(1 / 30))).isoformat(), username=random.choice(users), ) for experiment_id in range(self.fake_experiments_per_subject) @@ -186,10 +184,7 @@ def make(self, key): for trial_id in range(10): key["trial_id"] = trial_id self.insert1(dict(key, start_time=random.random() * 1e9)) - trial.insert( - dict(key, cond_idx=cond_idx, orientation=random.random() * 360) - for cond_idx in range(30) - ) + trial.insert(dict(key, cond_idx=cond_idx, orientation=random.random() * 360) for cond_idx in range(30)) class Ephys(dj.Imported): @@ -214,9 +209,7 @@ def _make_tuples(self, key): populate with random data """ random.seed(str(key)) - row = dict( - key, sampling_frequency=6000, duration=np.minimum(2, random.expovariate(1)) - ) + row = dict(key, sampling_frequency=6000, duration=np.minimum(2, random.expovariate(1))) self.insert1(row) number_samples = int(row["duration"] * row["sampling_frequency"] + 0.5) sub = self.Channel() @@ -388,9 +381,7 @@ class ComplexParent(dj.Lookup): class ComplexChild(dj.Lookup): - definition = "\n".join( - ["-> ComplexParent"] + ["child_id_{}: int".format(i + 1) for i in range(1)] - ) + definition = "\n".join(["-> ComplexParent"] + ["child_id_{}: int".format(i + 1) for i in range(1)]) contents = [tuple(i for i in range(9))] diff --git a/tests/schema_adapted.py b/tests/schema_adapted.py index 06e28c3d1..c7b5830c0 100644 --- a/tests/schema_adapted.py +++ b/tests/schema_adapted.py @@ -1,6 +1,5 @@ import inspect import json -import tempfile from pathlib import Path import networkx as nx diff --git a/tests/schema_external.py b/tests/schema_external.py index a9e86964f..cee92d6cd 100644 --- a/tests/schema_external.py +++ b/tests/schema_external.py @@ -3,7 +3,6 @@ """ import inspect -import tempfile import numpy as np diff --git a/tests/schema_simple.py b/tests/schema_simple.py index 5e5137db5..82d7695ff 100644 --- a/tests/schema_simple.py +++ b/tests/schema_simple.py @@ -83,10 +83,7 @@ def make(self, key): sigma = random.lognormvariate(0, 4) n = random.randint(0, 10) self.insert1(dict(key, mu=mu, sigma=sigma, n=n)) - sub.insert( - dict(key, id_c=j, value=random.normalvariate(mu, sigma)) - for j in range(n) - ) + sub.insert(dict(key, id_c=j, value=random.normalvariate(mu, sigma)) for j in range(n)) class L(dj.Lookup): @@ -159,11 +156,7 @@ def make(self, key): random.shuffle(bc_references) self.insert1(dict(key, **random.choice(l_contents))) - part_f.insert( - dict(key, id_f=i, **ref) - for i, ref in enumerate(bc_references) - if random.getrandbits(1) - ) + part_f.insert(dict(key, id_f=i, **ref) for i, ref in enumerate(bc_references) if random.getrandbits(1)) g_inserts = [dict(key, id_g=i, **ref) for i, ref in enumerate(l_contents)] part_g.insert(g_inserts) h_inserts = [dict(key, id_h=i) for i in range(4)] @@ -248,9 +241,7 @@ def populate_random(self, n=10): with self.connection.transaction: self.insert1(profile, ignore_extra_fields=True) for url in profile["website"]: - self.Website().insert1( - dict(ssn=profile["ssn"], url_hash=Website().insert1_url(url)) - ) + self.Website().insert1(dict(ssn=profile["ssn"], url_hash=Website().insert1_url(url))) class TTestUpdate(dj.Lookup): diff --git a/tests/schema_uuid.py b/tests/schema_uuid.py index 4e295bc86..75b9cd373 100644 --- a/tests/schema_uuid.py +++ b/tests/schema_uuid.py @@ -24,9 +24,7 @@ class Topic(dj.Manual): def add(self, topic): """add a new topic with a its UUID""" - self.insert1( - dict(topic_id=uuid.uuid5(top_level_namespace_id, topic), topic=topic) - ) + self.insert1(dict(topic_id=uuid.uuid5(top_level_namespace_id, topic), topic=topic)) class Item(dj.Computed): @@ -41,9 +39,7 @@ class Item(dj.Computed): def make(self, key): for word in ("Habenula", "Hippocampus", "Hypothalamus", "Hypophysis"): - self.insert1( - dict(key, word=word, item_id=uuid.uuid5(key["topic_id"], word)) - ) + self.insert1(dict(key, word=word, item_id=uuid.uuid5(key["topic_id"], word))) LOCALS_UUID = {k: v for k, v in locals().items() if inspect.isclass(v)} diff --git a/tests/test_adapted_attributes.py b/tests/test_adapted_attributes.py index ffd137795..1060a50ed 100644 --- a/tests/test_adapted_attributes.py +++ b/tests/test_adapted_attributes.py @@ -1,5 +1,3 @@ -import os -import tempfile from itertools import zip_longest import networkx as nx @@ -31,11 +29,7 @@ def schema_ad( tmpdir, schema_name, ): - dj.config["stores"] = { - "repo-s3": dict( - s3_creds, protocol="s3", location="adapted/repo", stage=str(tmpdir) - ) - } + dj.config["stores"] = {"repo-s3": dict(s3_creds, protocol="s3", location="adapted/repo", stage=str(tmpdir))} context = { **schema_adapted.LOCALS_ADAPTED, "graph": adapted_graph_instance, @@ -60,9 +54,7 @@ def local_schema(schema_ad, schema_name): @pytest.fixture def schema_virtual_module(schema_ad, adapted_graph_instance, schema_name): """Fixture for testing virtual modules""" - schema_virtual_module = dj.VirtualModule( - "virtual_module", schema_name, add_objects={"graph": adapted_graph_instance} - ) + schema_virtual_module = dj.VirtualModule("virtual_module", schema_name, add_objects={"graph": adapted_graph_instance}) return schema_virtual_module diff --git a/tests/test_admin.py b/tests/test_admin.py index b7fa15a33..b600b21e4 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -24,10 +24,7 @@ def user_alice(db_creds_root) -> dict: password="oldpass", ) root_conn.query(f"DROP USER IF EXISTS '{new_credentials['user']}'@'%%';") - root_conn.query( - f"CREATE USER '{new_credentials['user']}'@'%%' " - f"IDENTIFIED BY '{new_credentials['password']}';" - ) + root_conn.query(f"CREATE USER '{new_credentials['user']}'@'%%' " f"IDENTIFIED BY '{new_credentials['password']}';") # test the connection dj.Connection(**new_credentials) diff --git a/tests/test_aggr_regressions.py b/tests/test_aggr_regressions.py index ea740cd39..afbcdda18 100644 --- a/tests/test_aggr_regressions.py +++ b/tests/test_aggr_regressions.py @@ -123,8 +123,6 @@ def test_left_join_len(schema_uuid): Item.populate() Topic().add("jeff2") Topic().add("jeff3") - q = Topic.join( - Item - dict(topic_id=uuid.uuid5(top_level_namespace_id, "jeff")), left=True - ) + q = Topic.join(Item - dict(topic_id=uuid.uuid5(top_level_namespace_id, "jeff")), left=True) qf = q.fetch() assert len(q) == len(qf) diff --git a/tests/test_alter.py b/tests/test_alter.py index 375d31d55..952856010 100644 --- a/tests/test_alter.py +++ b/tests/test_alter.py @@ -2,7 +2,6 @@ import pytest -import datajoint as dj from . import schema as schema_any_module from .schema_alter import LOCALS_ALTER, Experiment, Parent @@ -24,33 +23,21 @@ def schema_alter(connection_test, schema_any): class TestAlter: def verify_alter(self, schema_alter, table, attribute_sql): - definition_original = schema_alter.connection.query( - f"SHOW CREATE TABLE {table.full_table_name}" - ).fetchone()[1] + definition_original = schema_alter.connection.query(f"SHOW CREATE TABLE {table.full_table_name}").fetchone()[1] table.definition = table.definition_new table.alter(prompt=False) - definition_new = schema_alter.connection.query( - f"SHOW CREATE TABLE {table.full_table_name}" - ).fetchone()[1] - assert ( - re.sub(f"{attribute_sql},\n ", "", definition_new) == definition_original - ) + definition_new = schema_alter.connection.query(f"SHOW CREATE TABLE {table.full_table_name}").fetchone()[1] + assert re.sub(f"{attribute_sql},\n ", "", definition_new) == definition_original def test_alter(self, schema_alter): - original = schema_alter.connection.query( - "SHOW CREATE TABLE " + Experiment.full_table_name - ).fetchone()[1] + original = schema_alter.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1] Experiment.definition = Experiment.definition1 Experiment.alter(prompt=False, context=COMBINED_CONTEXT) - altered = schema_alter.connection.query( - "SHOW CREATE TABLE " + Experiment.full_table_name - ).fetchone()[1] + altered = schema_alter.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1] assert original != altered Experiment.definition = Experiment.original_definition Experiment().alter(prompt=False, context=COMBINED_CONTEXT) - restored = schema_alter.connection.query( - "SHOW CREATE TABLE " + Experiment.full_table_name - ).fetchone()[1] + restored = schema_alter.connection.query("SHOW CREATE TABLE " + Experiment.full_table_name).fetchone()[1] assert altered != restored assert original == restored @@ -58,9 +45,7 @@ def test_alter_part(self, schema_alter): """ https://github.com/datajoint/datajoint-python/issues/936 """ - self.verify_alter( - schema_alter, table=Parent.Child, attribute_sql="`child_id` .* DEFAULT NULL" - ) + self.verify_alter(schema_alter, table=Parent.Child, attribute_sql="`child_id` .* DEFAULT NULL") self.verify_alter( schema_alter, table=Parent.Grandchild, diff --git a/tests/test_attach.py b/tests/test_attach.py index 362db6933..85737ec60 100644 --- a/tests/test_attach.py +++ b/tests/test_attach.py @@ -1,7 +1,6 @@ import os from pathlib import Path -import pytest from .schema_external import Attach @@ -23,9 +22,7 @@ def test_attach_attributes(schema_ext, minio_client, tmpdir_factory): table.insert1(dict(attach=i, img=attach1, txt=attach2)) download_folder = Path(tmpdir_factory.mktemp("download")) - keys, path1, path2 = table.fetch( - "KEY", "img", "txt", download_path=download_folder, order_by="KEY" - ) + keys, path1, path2 = table.fetch("KEY", "img", "txt", download_path=download_folder, order_by="KEY") # verify that different attachment are renamed if their filenames collide assert path1[0] != path2[0] @@ -61,8 +58,6 @@ def test_return_string(schema_ext, minio_client, tmpdir_factory): table.insert1(dict(attach=2, img=attach1, txt=attach2)) download_folder = Path(tmpdir_factory.mktemp("download")) - keys, path1, path2 = table.fetch( - "KEY", "img", "txt", download_path=download_folder, order_by="KEY" - ) + keys, path1, path2 = table.fetch("KEY", "img", "txt", download_path=download_folder, order_by="KEY") assert isinstance(path1[0], str) diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py index 899d90d9e..7dd6041a7 100644 --- a/tests/test_autopopulate.py +++ b/tests/test_autopopulate.py @@ -1,11 +1,8 @@ -import pymysql import pytest import datajoint as dj from datajoint import DataJointError -from . import schema - def test_populate(trial, subject, experiment, ephys, channel): # test simple populate diff --git a/tests/test_blob.py b/tests/test_blob.py index 7c790db75..6e5b9bd78 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -61,27 +61,19 @@ def test_pack(): x = -255 y = unpack(pack(x)) - assert ( - x == y and isinstance(y, int) and not isinstance(y, np.ndarray) - ), "Scalar int did not match" + assert x == y and isinstance(y, int) and not isinstance(y, np.ndarray), "Scalar int did not match" x = -25523987234234287910987234987098245697129798713407812347 y = unpack(pack(x)) - assert ( - x == y and isinstance(y, int) and not isinstance(y, np.ndarray) - ), "Unbounded int did not match" + assert x == y and isinstance(y, int) and not isinstance(y, np.ndarray), "Unbounded int did not match" x = 7.0 y = unpack(pack(x)) - assert ( - x == y and isinstance(y, float) and not isinstance(y, np.ndarray) - ), "Scalar float did not match" + assert x == y and isinstance(y, float) and not isinstance(y, np.ndarray), "Scalar float did not match" x = 7j y = unpack(pack(x)) - assert ( - x == y and isinstance(y, complex) and not isinstance(y, np.ndarray) - ), "Complex scalar did not match" + assert x == y and isinstance(y, complex) and not isinstance(y, np.ndarray), "Complex scalar did not match" x = True assert unpack(pack(x)) is True, "Scalar bool did not match" @@ -98,9 +90,7 @@ def test_pack(): } y = unpack(pack(x)) assert x == y, "Dict do not match!" - assert not isinstance( - ["range"][0], np.ndarray - ), "Scalar int was coerced into array." + assert not isinstance(["range"][0], np.ndarray), "Scalar int was coerced into array." x = uuid.uuid4() assert x == unpack(pack(x)), "UUID did not match" @@ -142,9 +132,7 @@ def test_pack(): assert x == unpack(pack(x)), "String object did not pack/unpack correctly" x = np.array(["yes"]) - assert x == unpack( - pack(x) - ), "Numpy string array object did not pack/unpack correctly" + assert x == unpack(pack(x)), "Numpy string array object did not pack/unpack correctly" x = np.datetime64("1998").astype("datetime64[us]") assert x == unpack(pack(x)) diff --git a/tests/test_blob_matlab.py b/tests/test_blob_matlab.py index 081841fb4..80a005659 100644 --- a/tests/test_blob_matlab.py +++ b/tests/test_blob_matlab.py @@ -44,9 +44,7 @@ def insert_blobs(schema): (6,'3D uint8 array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000009000000000000000102030405060708090A0B0C0D0E0F101112131415161718), (7,'3D complex array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000006000000010000000000000000C0724000000000000028C000000000000038C0000000000000000000000000000038C0000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000AA4C58E87AB62B400000000000000000AA4C58E87AB62BC0000000000000008000000000000052400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000008000000000000052C000000000000000800000000000000080000000000000008000000000000000800000000000000080 ); - """.format( - table_name=Blob.full_table_name - ) + """.format(table_name=Blob.full_table_name) ) @@ -84,9 +82,7 @@ def test_complex_matlab_blobs(schema_blob_pop): assert_array_equal(blob, np.array([["string1", "string2"]])) assert_array_equal(blob, unpack(pack(blob))) - blob = blobs[ - 3 - ] # 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) + blob = blobs[3] # 'struct array' struct('a', {1,2}, 'b', {struct('c', magic(3)), struct('C', magic(5))}) assert isinstance(blob, dj.MatStruct) assert tuple(blob.dtype.names) == ("a", "b") assert_array_equal(blob.a[0, 0], np.array([[1.0]])) @@ -117,17 +113,13 @@ def test_complex_matlab_squeeze(schema_blob_pop): """ test correct de-serialization of various blob types """ - blob = (Blob & "id=1").fetch1( - "blob", squeeze=True - ) # 'simple string' 'character string' + blob = (Blob & "id=1").fetch1("blob", squeeze=True) # 'simple string' 'character string' assert blob == "character string" blob = (Blob & "id=2").fetch1("blob", squeeze=True) # '1D vector' 1:15:180 assert_array_equal(blob, np.r_[1:180:15]) - blob = (Blob & "id=3").fetch1( - "blob", squeeze=True - ) # 'string array' {'string1' 'string2'} + blob = (Blob & "id=3").fetch1("blob", squeeze=True) # 'string array' {'string1' 'string2'} assert isinstance(blob, dj.MatCell) assert_array_equal(blob, np.array(["string1", "string2"])) @@ -148,9 +140,7 @@ def test_complex_matlab_squeeze(schema_blob_pop): assert isinstance(blob[1].b, dj.MatStruct) assert tuple(blob[1].b.C.item().shape) == (5, 5) - blob = (Blob & "id=5").fetch1( - "blob", squeeze=True - ) # '3D double array' reshape(1:24, [2,3,4]) + blob = (Blob & "id=5").fetch1("blob", squeeze=True) # '3D double array' reshape(1:24, [2,3,4]) assert np.array_equal(blob, np.r_[1:25].reshape((2, 3, 4), order="F")) assert blob.dtype == "float64" diff --git a/tests/test_cascading_delete.py b/tests/test_cascading_delete.py index 71216fcb2..642ebd0e8 100644 --- a/tests/test_cascading_delete.py +++ b/tests/test_cascading_delete.py @@ -19,9 +19,7 @@ def schema_simp_pop(schema_simp): def test_delete_tree(schema_simp_pop): assert not dj.config["safemode"], "safemode must be off for testing" - assert ( - L() and A() and B() and B.C() and D() and E() and E.F() - ), "schema is not populated" + assert L() and A() and B() and B.C() and D() and E() and E.F(), "schema is not populated" A().delete() assert not A() or B() or B.C() or D() or E() or E.F(), "incomplete delete" @@ -32,16 +30,12 @@ def test_stepwise_delete(schema_simp_pop): B.C().delete(force=True) assert not B.C(), "failed to delete child tables" B().delete() - assert ( - not B() - ), "failed to delete from the parent table following child table deletion" + assert not B(), "failed to delete from the parent table following child table deletion" def test_delete_tree_restricted(schema_simp_pop): assert not dj.config["safemode"], "safemode must be off for testing" - assert ( - L() and A() and B() and B.C() and D() and E() and E.F() - ), "schema is not populated" + assert L() and A() and B() and B.C() and D() and E() and E.F(), "schema is not populated" cond = "cond_in_a" rel = A() & cond rest = dict( @@ -53,9 +47,7 @@ def test_delete_tree_restricted(schema_simp_pop): F=len(E.F() - rel), ) rel.delete() - assert not ( - rel or B() & rel or B.C() & rel or D() & rel or E() & rel or (E.F() & rel) - ), "incomplete delete" + assert not (rel or B() & rel or B.C() & rel or D() & rel or E() & rel or (E.F() & rel)), "incomplete delete" assert len(A()) == rest["A"], "invalid delete restriction" assert len(B()) == rest["B"], "invalid delete restriction" assert len(B.C()) == rest["C"], "invalid delete restriction" @@ -66,9 +58,7 @@ def test_delete_tree_restricted(schema_simp_pop): def test_delete_lookup(schema_simp_pop): assert not dj.config["safemode"], "safemode must be off for testing" - assert bool( - L() and A() and B() and B.C() and D() and E() and E.F() - ), "schema is not populated" + assert bool(L() and A() and B() and B.C() and D() and E() and E.F()), "schema is not populated" L().delete() assert not bool(L() or D() or E() or E.F()), "incomplete delete" A().delete() # delete all is necessary because delete L deletes from subtables. @@ -76,9 +66,7 @@ def test_delete_lookup(schema_simp_pop): def test_delete_lookup_restricted(schema_simp_pop): assert not dj.config["safemode"], "safemode must be off for testing" - assert ( - L() and A() and B() and B.C() and D() and E() and E.F() - ), "schema is not populated" + assert L() and A() and B() and B.C() and D() and E() and E.F(), "schema is not populated" rel = L() & "cond_in_l" original_count = len(L()) deleted_count = len(rel) @@ -96,10 +84,7 @@ def test_delete_complex_keys(schema_any): child_key_count = 1 restriction = dict( {"parent_id_{}".format(i + 1): i for i in range(parent_key_count)}, - **{ - "child_id_{}".format(i + 1): (i + parent_key_count) - for i in range(child_key_count) - }, + **{"child_id_{}".format(i + 1): (i + parent_key_count) for i in range(child_key_count)}, ) assert len(ComplexParent & restriction) == 1, "Parent record missing" assert len(ComplexChild & restriction) == 1, "Child record missing" diff --git a/tests/test_cli.py b/tests/test_cli.py index be0faf64d..2fb86d796 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,7 +2,6 @@ Collection of test cases to test the dj cli """ -import json import subprocess import pytest diff --git a/tests/test_connection.py b/tests/test_connection.py index db301d9af..8a30d4a46 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -88,13 +88,9 @@ def test_transaction_rollback(schema_tx, connection_test): raise DataJointError("Testing rollback") except DataJointError: pass - assert ( - len(Subjects()) == 1 - ), "Length is not 1. Expected because rollback should have happened." + assert len(Subjects()) == 1, "Length is not 1. Expected because rollback should have happened." - assert ( - len(Subjects & "subject_id = 2") == 0 - ), "Length is not 0. Expected because rollback should have happened." + assert len(Subjects & "subject_id = 2") == 0, "Length is not 0. Expected because rollback should have happened." def test_cancel(schema_tx, connection_test): @@ -108,9 +104,5 @@ def test_cancel(schema_tx, connection_test): connection_test.start_transaction() Subjects.insert1(tmp[1]) connection_test.cancel_transaction() - assert ( - len(Subjects()) == 1 - ), "Length is not 1. Expected because rollback should have happened." - assert ( - len(Subjects & "subject_id = 2") == 0 - ), "Length is not 0. Expected because rollback should have happened." + assert len(Subjects()) == 1, "Length is not 1. Expected because rollback should have happened." + assert len(Subjects & "subject_id = 2") == 0, "Length is not 0. Expected because rollback should have happened." diff --git a/tests/test_declare.py b/tests/test_declare.py index 828021939..ed6dcbd17 100644 --- a/tests/test_declare.py +++ b/tests/test_declare.py @@ -178,48 +178,30 @@ def test_dependencies(schema_any): assert set(experiment.parents(primary=False)) == {user.full_table_name} assert experiment.full_table_name in user.children(primary=False) assert set(experiment.parents(primary=False)) == {user.full_table_name} - assert set( - s.full_table_name for s in experiment.parents(primary=False, as_objects=True) - ) == {user.full_table_name} + assert set(s.full_table_name for s in experiment.parents(primary=False, as_objects=True)) == {user.full_table_name} assert experiment.full_table_name in subject.descendants() - assert experiment.full_table_name in { - s.full_table_name for s in subject.descendants(as_objects=True) - } + assert experiment.full_table_name in {s.full_table_name for s in subject.descendants(as_objects=True)} assert subject.full_table_name in experiment.ancestors() - assert subject.full_table_name in { - s.full_table_name for s in experiment.ancestors(as_objects=True) - } + assert subject.full_table_name in {s.full_table_name for s in experiment.ancestors(as_objects=True)} assert trial.full_table_name in experiment.descendants() - assert trial.full_table_name in { - s.full_table_name for s in experiment.descendants(as_objects=True) - } + assert trial.full_table_name in {s.full_table_name for s in experiment.descendants(as_objects=True)} assert experiment.full_table_name in trial.ancestors() - assert experiment.full_table_name in { - s.full_table_name for s in trial.ancestors(as_objects=True) - } + assert experiment.full_table_name in {s.full_table_name for s in trial.ancestors(as_objects=True)} assert set(trial.children(primary=True)) == { ephys.full_table_name, trial.Condition.full_table_name, } assert set(trial.parts()) == {trial.Condition.full_table_name} - assert set(s.full_table_name for s in trial.parts(as_objects=True)) == { - trial.Condition.full_table_name - } + assert set(s.full_table_name for s in trial.parts(as_objects=True)) == {trial.Condition.full_table_name} assert set(ephys.parents(primary=True)) == {trial.full_table_name} - assert set( - s.full_table_name for s in ephys.parents(primary=True, as_objects=True) - ) == {trial.full_table_name} + assert set(s.full_table_name for s in ephys.parents(primary=True, as_objects=True)) == {trial.full_table_name} assert set(ephys.children(primary=True)) == {channel.full_table_name} - assert set( - s.full_table_name for s in ephys.children(primary=True, as_objects=True) - ) == {channel.full_table_name} + assert set(s.full_table_name for s in ephys.children(primary=True, as_objects=True)) == {channel.full_table_name} assert set(channel.parents(primary=True)) == {ephys.full_table_name} - assert set( - s.full_table_name for s in channel.parents(primary=True, as_objects=True) - ) == {ephys.full_table_name} + assert set(s.full_table_name for s in channel.parents(primary=True, as_objects=True)) == {ephys.full_table_name} def test_descendants_only_contain_part_table(schema_any): @@ -388,42 +370,28 @@ class Table_With_Underscores(dj.Manual): """ schema_any(TableNoUnderscores) - with pytest.raises( - dj.DataJointError, match="must be alphanumeric in CamelCase" - ) as e: + with pytest.raises(dj.DataJointError, match="must be alphanumeric in CamelCase") as e: schema_any(Table_With_Underscores) def test_add_hidden_timestamp_default_value(): config_val = config.get("add_hidden_timestamp") - assert ( - config_val is not None and not config_val - ), "Default value for add_hidden_timestamp is not False" + assert config_val is not None and not config_val, "Default value for add_hidden_timestamp is not False" def test_add_hidden_timestamp_enabled(enable_add_hidden_timestamp, schema_any): assert config["add_hidden_timestamp"], "add_hidden_timestamp is not enabled" msg = f"{Experiment().heading._attributes=}" - assert any( - a.name.endswith("_timestamp") for a in Experiment().heading._attributes.values() - ), msg - assert any( - a.name.startswith("_") for a in Experiment().heading._attributes.values() - ), msg + assert any(a.name.endswith("_timestamp") for a in Experiment().heading._attributes.values()), msg + assert any(a.name.startswith("_") for a in Experiment().heading._attributes.values()), msg assert any(a.is_hidden for a in Experiment().heading._attributes.values()), msg assert not any(a.is_hidden for a in Experiment().heading.attributes.values()), msg def test_add_hidden_timestamp_disabled(disable_add_hidden_timestamp, schema_any): - assert not config[ - "add_hidden_timestamp" - ], "expected add_hidden_timestamp to be False" + assert not config["add_hidden_timestamp"], "expected add_hidden_timestamp to be False" msg = f"{Experiment().heading._attributes=}" - assert not any( - a.name.endswith("_timestamp") for a in Experiment().heading._attributes.values() - ), msg - assert not any( - a.name.startswith("_") for a in Experiment().heading._attributes.values() - ), msg + assert not any(a.name.endswith("_timestamp") for a in Experiment().heading._attributes.values()), msg + assert not any(a.name.startswith("_") for a in Experiment().heading._attributes.values()), msg assert not any(a.is_hidden for a in Experiment().heading._attributes.values()), msg assert not any(a.is_hidden for a in Experiment().heading.attributes.values()), msg diff --git a/tests/test_erd.py b/tests/test_erd.py index e2344cf8a..9bf59c334 100644 --- a/tests/test_erd.py +++ b/tests/test_erd.py @@ -1,7 +1,7 @@ import datajoint as dj from .schema_advanced import * -from .schema_simple import LOCALS_SIMPLE, A, B, D, E, G, L, OutfitLaunch +from .schema_simple import LOCALS_SIMPLE, A, B, D, E, G, L def test_decorator(schema_simp): @@ -20,9 +20,7 @@ def test_dependencies(schema_simp): assert set(A().children()) == set([B.full_table_name, D.full_table_name]) assert set(D().parents(primary=True)) == set([A.full_table_name]) assert set(D().parents(primary=False)) == set([L.full_table_name]) - assert set(deps.descendants(L.full_table_name)).issubset( - cls.full_table_name for cls in (L, D, E, E.F, E.G, E.H, E.M, G) - ) + assert set(deps.descendants(L.full_table_name)).issubset(cls.full_table_name for cls in (L, D, E, E.F, E.G, E.H, E.M, G)) def test_erd(schema_simp): @@ -39,14 +37,10 @@ def test_erd_algebra(schema_simp): erd3 = erd1 * erd2 erd4 = (erd0 + E).add_parts() - B - E assert erd0.nodes_to_show == set(cls.full_table_name for cls in [B]) - assert erd1.nodes_to_show == set( - cls.full_table_name for cls in (B, B.C, E, E.F, E.G, E.H, E.M, G) - ) + assert erd1.nodes_to_show == set(cls.full_table_name for cls in (B, B.C, E, E.F, E.G, E.H, E.M, G)) assert erd2.nodes_to_show == set(cls.full_table_name for cls in (A, B, D, E, L)) assert erd3.nodes_to_show == set(cls.full_table_name for cls in (B, E)) - assert erd4.nodes_to_show == set( - cls.full_table_name for cls in (B.C, E.F, E.G, E.H, E.M) - ) + assert erd4.nodes_to_show == set(cls.full_table_name for cls in (B.C, E.F, E.G, E.H, E.M)) def test_repr_svg(schema_adv): diff --git a/tests/test_external.py b/tests/test_external.py index 10021c0aa..9767857ac 100644 --- a/tests/test_external.py +++ b/tests/test_external.py @@ -14,9 +14,7 @@ def test_external_put(schema_ext, mock_stores, mock_cache): """ external storage put and get and remove """ - ext = ExternalTable( - schema_ext.connection, store="raw", database=schema_ext.database - ) + ext = ExternalTable(schema_ext.connection, store="raw", database=schema_ext.database) initial_length = len(ext) input_ = np.random.randn(3, 7, 8) count = 7 @@ -41,9 +39,7 @@ def test_s3_leading_slash(self, schema_ext, mock_stores, mock_cache, minio_clien """ self._leading_slash(schema_ext, index=100, store="share") - def test_file_leading_slash( - self, schema_ext, mock_stores, mock_cache, minio_client - ): + def test_file_leading_slash(self, schema_ext, mock_stores, mock_cache, minio_client): """ File external storage configured with leading slash """ @@ -56,58 +52,42 @@ def _leading_slash(self, schema_ext, index, store): id = index dj.config["stores"][store]["location"] = "leading/slash/test" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 1 dj.config["stores"][store]["location"] = "/leading/slash/test" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 2 dj.config["stores"][store]["location"] = "leading\\slash\\test" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 3 dj.config["stores"][store]["location"] = "f:\\leading\\slash\\test" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 4 dj.config["stores"][store]["location"] = "f:\\leading/slash\\test" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 5 dj.config["stores"][store]["location"] = "/" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 6 dj.config["stores"][store]["location"] = "C:\\" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) id = index + 7 dj.config["stores"][store]["location"] = "" SimpleRemote.insert([{"simple": id, "item": value}]) - assert np.array_equal( - value, (SimpleRemote & "simple={}".format(id)).fetch1("item") - ) + assert np.array_equal(value, (SimpleRemote & "simple={}".format(id)).fetch1("item")) dj.config["stores"][store]["location"] = oldConfig diff --git a/tests/test_external_class.py b/tests/test_external_class.py index 84597e52f..ed0ec010b 100644 --- a/tests/test_external_class.py +++ b/tests/test_external_class.py @@ -38,13 +38,8 @@ def test_populate(schema_ext, mock_stores): image = schema_external.Image() image.populate() remaining, total = image.progress() - assert ( - total == len(schema_external.Dimension() * schema_external.Seed()) - and remaining == 0 - ) - for img, neg, dimensions in zip( - *(image * schema_external.Dimension()).fetch("img", "neg", "dimensions") - ): + assert total == len(schema_external.Dimension() * schema_external.Seed()) and remaining == 0 + for img, neg, dimensions in zip(*(image * schema_external.Dimension()).fetch("img", "neg", "dimensions")): assert list(img.shape) == list(dimensions) assert_almost_equal(img, -neg) image.delete() diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 7df767028..26a9229a5 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -1,11 +1,7 @@ import decimal -import io import itertools -import logging import os -import warnings from operator import itemgetter -from typing import List import numpy as np import pandas @@ -50,9 +46,7 @@ def test_order_by(lang, languages): languages.sort(key=itemgetter(1), reverse=ord_lang == "DESC") languages.sort(key=itemgetter(0), reverse=ord_name == "DESC") for c, l in zip(cur, languages): - assert np.all( - cc == ll for cc, ll in zip(c, l) - ), "Sorting order is different" + assert np.all(cc == ll for cc, ll in zip(c, l)), "Sorting order is different" def test_order_by_default(lang, languages): @@ -119,9 +113,7 @@ def test_iter(lang, languages): # now as dict cur = lang.fetch(as_dict=True, order_by=("language", "name DESC")) for row, (tname, tlang) in list(zip(cur, languages)): - assert ( - row["name"] == tname and row["language"] == tlang - ), "Values are not the same" + assert row["name"] == tname and row["language"] == tlang, "Values are not the same" def test_keys(lang, languages): @@ -272,9 +264,7 @@ def test_fetch_format(subject): subject_notes, key, real_id = subject.fetch("subject_notes", dj.key, "real_id") - np.testing.assert_array_equal( - sorted(subject_notes), sorted(tmp["subject_notes"]) - ) + np.testing.assert_array_equal(sorted(subject_notes), sorted(tmp["subject_notes"])) np.testing.assert_array_equal(sorted(real_id), sorted(tmp["real_id"])) list1 = sorted(key, key=itemgetter("subject_id")) for l1, l2 in zip(list1, list2): diff --git a/tests/test_filepath.py b/tests/test_filepath.py index cc3db2cc2..56dbd9688 100644 --- a/tests/test_filepath.py +++ b/tests/test_filepath.py @@ -30,9 +30,7 @@ def test_path_match(schema_ext, enable_filepath_feature, minio_client, store="re assert not managed_file.exists() # check filepath - assert (ext & {"hash": uuid}).fetch1("filepath") == str( - managed_file.relative_to(stage_path).as_posix() - ) + assert (ext & {"hash": uuid}).fetch1("filepath") == str(managed_file.relative_to(stage_path).as_posix()) # # Download the file and check its contents. restored_path, checksum = ext.download_filepath(uuid) @@ -116,9 +114,7 @@ def test_duplicate_error(schema_ext, store): class TestFilepath: - def _test_filepath_class( - self, table=Filepath(), store="repo", verify_checksum=True - ): + def _test_filepath_class(self, table=Filepath(), store="repo", verify_checksum=True): if not verify_checksum: dj.config["filepath_checksum_size_limit"] = 0 stage_path = dj.config["stores"][store]["stage"] @@ -181,9 +177,7 @@ def test_filepath_class_no_checksum(self, schema_ext, enable_filepath_feature): logger = logging.getLogger("datajoint") log_capture = io.StringIO() stream_handler = logging.StreamHandler(log_capture) - log_format = logging.Formatter( - "[%(asctime)s][%(funcName)s][%(levelname)s]: %(message)s" - ) + log_format = logging.Formatter("[%(asctime)s][%(funcName)s][%(levelname)s]: %(message)s") stream_handler.setFormatter(log_format) stream_handler.set_name("test_limit_warning") logger.addHandler(stream_handler) @@ -240,9 +234,7 @@ def test_delete_without_files( schema_ext.external[store].delete(delete_external_files=False) -def test_return_string( - schema_ext, enable_filepath_feature, table=Filepath(), store="repo" -): +def test_return_string(schema_ext, enable_filepath_feature, table=Filepath(), store="repo"): """test returning string on fetch""" stage_path = dj.config["stores"][store]["stage"] # create a mock file diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index b271c6c1f..6049bd53f 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -25,9 +25,7 @@ def test_describe(schema_adv): """real_definition should match original definition""" for rel in (LocalSynapse, GlobalSynapse): describe = rel.describe() - s1 = declare(rel.full_table_name, rel.definition, schema_adv.context)[0].split( - "\n" - ) + s1 = declare(rel.full_table_name, rel.definition, schema_adv.context)[0].split("\n") s2 = declare(rel.full_table_name, describe, globals())[0].split("\n") for c1, c2 in zip(s1, s2): assert c1 == c2 diff --git a/tests/test_jobs.py b/tests/test_jobs.py index dc363076d..6ce70ef16 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -1,7 +1,6 @@ import random import string -import pytest import datajoint as dj from datajoint.jobs import ERROR_MESSAGE_LENGTH, TRUNCATION_APPENDIX @@ -19,9 +18,7 @@ def test_reserve_job(subject, schema_any): # refuse jobs for key in subject.fetch("KEY"): - assert not schema_any.jobs.reserve( - table_name, key - ), "failed to respect reservation" + assert not schema_any.jobs.reserve(table_name, key), "failed to respect reservation" # complete jobs for key in subject.fetch("KEY"): @@ -38,9 +35,7 @@ def test_reserve_job(subject, schema_any): # refuse jobs with errors for key in subject.fetch("KEY"): - assert not schema_any.jobs.reserve( - table_name, key - ), "failed to ignore error jobs" + assert not schema_any.jobs.reserve(table_name, key), "failed to ignore error jobs" # clear error jobs (schema_any.jobs & dict(status="error")).delete() @@ -95,12 +90,8 @@ def test_suppress_dj_errors(schema_any): def test_long_error_message(subject, schema_any): # create long error message - long_error_message = "".join( - random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH + 100) - ) - short_error_message = "".join( - random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH // 2) - ) + long_error_message = "".join(random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH + 100)) + short_error_message = "".join(random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH // 2)) assert subject table_name = "fake_table" @@ -110,12 +101,8 @@ def test_long_error_message(subject, schema_any): schema_any.jobs.reserve(table_name, key) schema_any.jobs.error(table_name, key, long_error_message) error_message = schema_any.jobs.fetch1("error_message") - assert ( - len(error_message) == ERROR_MESSAGE_LENGTH - ), "error message is longer than max allowed" - assert error_message.endswith( - TRUNCATION_APPENDIX - ), "appropriate ending missing for truncated error message" + assert len(error_message) == ERROR_MESSAGE_LENGTH, "error message is longer than max allowed" + assert error_message.endswith(TRUNCATION_APPENDIX), "appropriate ending missing for truncated error message" schema_any.jobs.delete() # test long error message @@ -123,20 +110,14 @@ def test_long_error_message(subject, schema_any): schema_any.jobs.error(table_name, key, short_error_message) error_message = schema_any.jobs.fetch1("error_message") assert error_message == short_error_message, "error messages do not agree" - assert not error_message.endswith( - TRUNCATION_APPENDIX - ), "error message should not be truncated" + assert not error_message.endswith(TRUNCATION_APPENDIX), "error message should not be truncated" schema_any.jobs.delete() def test_long_error_stack(subject, schema_any): # create long error stack - STACK_SIZE = ( - 89942 # Does not fit into small blob (should be 64k, but found to be higher) - ) - long_error_stack = "".join( - random.choice(string.ascii_letters) for _ in range(STACK_SIZE) - ) + STACK_SIZE = 89942 # Does not fit into small blob (should be 64k, but found to be higher) + long_error_stack = "".join(random.choice(string.ascii_letters) for _ in range(STACK_SIZE)) assert subject table_name = "fake_table" diff --git a/tests/test_json.py b/tests/test_json.py index 0a819b99e..b4b7247f1 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -67,9 +67,7 @@ class Team(dj.Lookup): @pytest.fixture def schema_json(connection_test, prefix): - schema = dj.Schema( - prefix + "_json", context=dict(Team=Team), connection=connection_test - ) + schema = dj.Schema(prefix + "_json", context=dict(Team=Team), connection=connection_test) schema(Team) yield schema schema.drop() @@ -131,13 +129,9 @@ def test_restrict(schema_json): assert (Team & {"car.safety_inspected": "false"}).fetch1("name") == "business" - assert (Team & {"car.safety_inspected:unsigned": False}).fetch1( - "name" - ) == "business" + assert (Team & {"car.safety_inspected:unsigned": False}).fetch1("name") == "business" - assert (Team & {"car.headlights[0].hyper_white": None}).fetch( - "name", order_by="name", as_dict=True - ) == [ + assert (Team & {"car.headlights[0].hyper_white": None}).fetch("name", order_by="name", as_dict=True) == [ {"name": "engineering"}, {"name": "marketing"}, ] # if entire record missing, JSON key is missing, or value set to JSON null @@ -146,79 +140,64 @@ def test_restrict(schema_json): assert (Team & {"car.tire_pressure": [34, 30, 27, 32]}).fetch1("name") == "business" - assert ( - Team & {"car.headlights[1]": {"side": "right", "hyper_white": True}} - ).fetch1("name") == "business" + assert (Team & {"car.headlights[1]": {"side": "right", "hyper_white": True}}).fetch1("name") == "business" # sql operators - assert (Team & "`car`->>'$.name' LIKE '%ching%'").fetch1( - "name" - ) == "business", "Missing substring" + assert (Team & "`car`->>'$.name' LIKE '%ching%'").fetch1("name") == "business", "Missing substring" assert (Team & "`car`->>'$.length' > 30").fetch1("name") == "business", "<= 30" - assert ( - Team & "JSON_VALUE(`car`, '$.safety_inspected' RETURNING UNSIGNED) = 0" - ).fetch1("name") == "business", "Has `safety_inspected` set to `true`" + assert (Team & "JSON_VALUE(`car`, '$.safety_inspected' RETURNING UNSIGNED) = 0").fetch1( + "name" + ) == "business", "Has `safety_inspected` set to `true`" assert (Team & "`car`->>'$.headlights[0].hyper_white' = 'null'").fetch1( "name" ) == "engineering", "Has 1st `headlight` with `hyper_white` not set to `null`" - assert (Team & "`car`->>'$.inspected' IS NOT NULL").fetch1( - "name" - ) == "engineering", "Missing `inspected` key" + assert (Team & "`car`->>'$.inspected' IS NOT NULL").fetch1("name") == "engineering", "Missing `inspected` key" assert (Team & "`car`->>'$.tire_pressure' = '[34, 30, 27, 32]'").fetch1( "name" ) == "business", "`tire_pressure` array did not match" - assert ( - Team - & """`car`->>'$.headlights[1]' = '{"side": "right", "hyper_white": true}'""" - ).fetch1("name") == "business", "2nd `headlight` object did not match" + assert (Team & """`car`->>'$.headlights[1]' = '{"side": "right", "hyper_white": true}'""").fetch1( + "name" + ) == "business", "2nd `headlight` object did not match" def test_proj(schema_json): # proj necessary since we need to rename indexed value into a proper attribute name - assert Team.proj(car_length="car.length").fetch( - as_dict=True, order_by="car_length" - ) == [ + assert Team.proj(car_length="car.length").fetch(as_dict=True, order_by="car_length") == [ {"name": "marketing", "car_length": None}, {"name": "business", "car_length": "100"}, {"name": "engineering", "car_length": "20.5"}, ] - assert Team.proj(car_length="car.length:decimal(4, 1)").fetch( - as_dict=True, order_by="car_length" - ) == [ + assert Team.proj(car_length="car.length:decimal(4, 1)").fetch(as_dict=True, order_by="car_length") == [ {"name": "marketing", "car_length": None}, {"name": "engineering", "car_length": 20.5}, {"name": "business", "car_length": 100.0}, ] - assert Team.proj( - car_width="JSON_VALUE(`car`, '$.length' RETURNING float) - 15" - ).fetch(as_dict=True, order_by="car_width") == [ + assert Team.proj(car_width="JSON_VALUE(`car`, '$.length' RETURNING float) - 15").fetch( + as_dict=True, order_by="car_width" + ) == [ {"name": "marketing", "car_width": None}, {"name": "engineering", "car_width": 5.5}, {"name": "business", "car_width": 85.0}, ] - assert ( - (Team & {"name": "engineering"}).proj(car_tire_pressure="car.tire_pressure") - ).fetch1("car_tire_pressure") == "[32, 31, 33, 34]" + assert ((Team & {"name": "engineering"}).proj(car_tire_pressure="car.tire_pressure")).fetch1( + "car_tire_pressure" + ) == "[32, 31, 33, 34]" assert np.array_equal( - Team.proj(car_inspected="car.inspected").fetch( - "car_inspected", order_by="name" - ), + Team.proj(car_inspected="car.inspected").fetch("car_inspected", order_by="name"), np.array([None, "true", None]), ) assert np.array_equal( - Team.proj(car_inspected="car.inspected:unsigned").fetch( - "car_inspected", order_by="name" - ), + Team.proj(car_inspected="car.inspected:unsigned").fetch("car_inspected", order_by="name"), np.array([None, 1, None]), ) diff --git a/tests/test_log.py b/tests/test_log.py index 4b6e64613..87905cd47 100644 --- a/tests/test_log.py +++ b/tests/test_log.py @@ -1,5 +1,3 @@ def test_log(schema_any): - ts, events = (schema_any.log & 'event like "Declared%%"').fetch( - "timestamp", "event" - ) + ts, events = (schema_any.log & 'event like "Declared%%"').fetch("timestamp", "event") assert len(ts) >= 2 diff --git a/tests/test_nan.py b/tests/test_nan.py index 25e4e332b..24e6d13da 100644 --- a/tests/test_nan.py +++ b/tests/test_nan.py @@ -14,9 +14,7 @@ class NanTest(dj.Manual): @pytest.fixture def schema_nan(connection_test, prefix): - schema = dj.Schema( - prefix + "_nantest", context=dict(NanTest=NanTest), connection=connection_test - ) + schema = dj.Schema(prefix + "_nantest", context=dict(NanTest=NanTest), connection=connection_test) schema(NanTest) yield schema schema.drop() @@ -40,13 +38,9 @@ def test_insert_nan(schema_nan_pop, arr_a): """Test fetching of null values""" b = NanTest().fetch("value", order_by="id") assert (np.isnan(arr_a) == np.isnan(b)).all(), "incorrect handling of Nans" - assert np.allclose( - arr_a[np.logical_not(np.isnan(arr_a))], b[np.logical_not(np.isnan(b))] - ), "incorrect storage of floats" + assert np.allclose(arr_a[np.logical_not(np.isnan(arr_a))], b[np.logical_not(np.isnan(b))]), "incorrect storage of floats" def test_nulls_do_not_affect_primary_keys(schema_nan_pop, arr_a): """Test against a case that previously caused a bug when skipping existing entries.""" - NanTest().insert( - ((i, value) for i, value in enumerate(arr_a)), skip_duplicates=True - ) + NanTest().insert(((i, value) for i, value in enumerate(arr_a)), skip_duplicates=True) diff --git a/tests/test_privileges.py b/tests/test_privileges.py index 2bf67a386..ed5925963 100644 --- a/tests/test_privileges.py +++ b/tests/test_privileges.py @@ -1,5 +1,3 @@ -import os - import pytest import datajoint as dj @@ -79,25 +77,17 @@ class TestUnprivileged: def test_fail_create_schema(self, connection_djview): """creating a schema with no CREATE privilege""" with pytest.raises(dj.DataJointError): - return dj.Schema( - "forbidden_schema", namespace, connection=connection_djview - ) + return dj.Schema("forbidden_schema", namespace, connection=connection_djview) def test_insert_failure(self, connection_djview, schema_any): - unprivileged = dj.Schema( - schema_any.database, namespace, connection=connection_djview - ) + unprivileged = dj.Schema(schema_any.database, namespace, connection=connection_djview) unprivileged.spawn_missing_classes() - assert issubclass(Language, dj.Lookup) and len(Language()) == len( - schema.Language() - ), "failed to spawn missing classes" + assert issubclass(Language, dj.Lookup) and len(Language()) == len(schema.Language()), "failed to spawn missing classes" with pytest.raises(dj.DataJointError): Language().insert1(("Socrates", "Greek")) def test_failure_to_create_table(self, connection_djview, schema_any): - unprivileged = dj.Schema( - schema_any.database, namespace, connection=connection_djview - ) + unprivileged = dj.Schema(schema_any.database, namespace, connection=connection_djview) @unprivileged class Try(dj.Manual): @@ -113,8 +103,6 @@ class Try(dj.Manual): class TestSubset: def test_populate_activate(self, connection_djsubset, schema_priv, prefix): - schema_priv.activate( - f"{prefix}_schema_privileges", create_schema=True, create_tables=False - ) + schema_priv.activate(f"{prefix}_schema_privileges", create_schema=True, create_tables=False) schema_privileges.Child.populate() assert schema_privileges.Child.progress(display=False)[0] == 0 diff --git a/tests/test_relation.py b/tests/test_relation.py index 565e1eafa..50cc17020 100644 --- a/tests/test_relation.py +++ b/tests/test_relation.py @@ -182,9 +182,7 @@ def test_replace(subject): skip_duplicates=True, ) assert date != str((subject & key).fetch1("date_of_birth")), "inappropriate replace" - subject.insert1( - dict(key, real_id=7, date_of_birth=date, subject_notes=""), replace=True - ) + subject.insert1(dict(key, real_id=7, date_of_birth=date, subject_notes=""), replace=True) assert date == str((subject & key).fetch1("date_of_birth")), "replace failed" @@ -277,9 +275,7 @@ def relation_selector(attr): tiers = [dj.Imported, dj.Manual, dj.Lookup, dj.Computed] for name, rel in getmembers(schema, relation_selector): - assert re.match( - rel.tier_regexp, rel.table_name - ), "Regular expression does not match for {name}".format(name=name) + assert re.match(rel.tier_regexp, rel.table_name), "Regular expression does not match for {name}".format(name=name) for tier in tiers: assert issubclass(rel, tier) or not re.match( tier.tier_regexp, rel.table_name diff --git a/tests/test_relation_u.py b/tests/test_relation_u.py index 59cee0249..67304d36e 100644 --- a/tests/test_relation_u.py +++ b/tests/test_relation_u.py @@ -1,4 +1,3 @@ -import pytest from pytest import raises import datajoint as dj @@ -75,6 +74,4 @@ def test_aggr(schema_any, schema_simp): rel = ArgmaxTest() amax1 = (dj.U("val") * rel) & dj.U("secondary_key").aggr(rel, val="min(val)") amax2 = (dj.U("val") * rel) * dj.U("secondary_key").aggr(rel, val="min(val)") - assert ( - len(amax1) == len(amax2) == rel.n - ), "Aggregated argmax with join and restriction does not yield the same length." + assert len(amax1) == len(amax2) == rel.n, "Aggregated argmax with join and restriction does not yield the same length." diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py index 2dbea672e..04b364b02 100644 --- a/tests/test_relational_operand.py +++ b/tests/test_relational_operand.py @@ -61,17 +61,13 @@ def test_rename(schema_simp_pop): # test renaming x = B().proj(i="id_a") & "i in (1,2,3,4)" lenx = len(x) - assert len(x) == len( - B() & "id_a in (1,2,3,4)" - ), "incorrect restriction of renamed attributes" + assert len(x) == len(B() & "id_a in (1,2,3,4)"), "incorrect restriction of renamed attributes" assert len(x & "id_b in (1,2)") == len( B() & "id_b in (1,2) and id_a in (1,2,3,4)" ), "incorrect restriction of renamed restriction" assert len(x) == lenx, "restriction modified original" y = x.proj(j="i") - assert len(y) == len( - B() & "id_a in (1,2,3,4)" - ), "incorrect projection of restriction" + assert len(y) == len(B() & "id_a in (1,2,3,4)"), "incorrect projection of restriction" z = y & "j in (3, 4, 5, 6)" assert len(z) == len(B() & "id_a in (3,4)"), "incorrect nested subqueries" @@ -92,24 +88,16 @@ def test_join(schema_simp_pop): y = L() rel = x * y assert len(rel) == len(x) * len(y), "incorrect join" - assert set(x.heading.names).union(y.heading.names) == set( - rel.heading.names - ), "incorrect join heading" - assert set(x.primary_key).union(y.primary_key) == set( - rel.primary_key - ), "incorrect join primary_key" + assert set(x.heading.names).union(y.heading.names) == set(rel.heading.names), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set(rel.primary_key), "incorrect join primary_key" # Test cartesian product of restricted relations x = A() & "cond_in_a=1" y = L() & "cond_in_l=1" rel = x * y assert len(rel) == len(x) * len(y), "incorrect join" - assert set(x.heading.names).union(y.heading.names) == set( - rel.heading.names - ), "incorrect join heading" - assert set(x.primary_key).union(y.primary_key) == set( - rel.primary_key - ), "incorrect join primary_key" + assert set(x.heading.names).union(y.heading.names) == set(rel.heading.names), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set(rel.primary_key), "incorrect join primary_key" # Test join with common attributes cond = A() & "cond_in_a=1" @@ -118,36 +106,22 @@ def test_join(schema_simp_pop): rel = x * y assert len(rel) >= len(x) and len(rel) >= len(y), "incorrect join" assert not rel - cond, "incorrect join, restriction, or antijoin" - assert set(x.heading.names).union(y.heading.names) == set( - rel.heading.names - ), "incorrect join heading" - assert set(x.primary_key).union(y.primary_key) == set( - rel.primary_key - ), "incorrect join primary_key" + assert set(x.heading.names).union(y.heading.names) == set(rel.heading.names), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set(rel.primary_key), "incorrect join primary_key" # test renamed join - x = B().proj( - i="id_a" - ) # rename the common attribute to achieve full cartesian product + x = B().proj(i="id_a") # rename the common attribute to achieve full cartesian product y = D() rel = x * y assert len(rel) == len(x) * len(y), "incorrect join" - assert set(x.heading.names).union(y.heading.names) == set( - rel.heading.names - ), "incorrect join heading" - assert set(x.primary_key).union(y.primary_key) == set( - rel.primary_key - ), "incorrect join primary_key" + assert set(x.heading.names).union(y.heading.names) == set(rel.heading.names), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set(rel.primary_key), "incorrect join primary_key" x = B().proj(a="id_a") y = D() rel = x * y assert len(rel) == len(x) * len(y), "incorrect join" - assert set(x.heading.names).union(y.heading.names) == set( - rel.heading.names - ), "incorrect join heading" - assert set(x.primary_key).union(y.primary_key) == set( - rel.primary_key - ), "incorrect join primary_key" + assert set(x.heading.names).union(y.heading.names) == set(rel.heading.names), "incorrect join heading" + assert set(x.primary_key).union(y.primary_key) == set(rel.primary_key), "incorrect join primary_key" # test pairing # Approach 1: join then restrict @@ -187,22 +161,15 @@ def test_project(schema_simp_pop): # projection after restriction cond = L() & "cond_in_l" assert len(D() & cond) + len(D() - cond) == len(D()), "failed semijoin or antijoin" - assert len((D() & cond).proj()) == len((D() & cond)), ( - "projection failed: altered its argument" "s cardinality" - ) + assert len((D() & cond).proj()) == len((D() & cond)), "projection failed: altered its argument" "s cardinality" -def test_rename_non_dj_attribute( - connection_test, schema_simp_pop, schema_any_pop, prefix -): +def test_rename_non_dj_attribute(connection_test, schema_simp_pop, schema_any_pop, prefix): schema = prefix + "_test1" - connection_test.query( - f"CREATE TABLE {schema}.test_table (oldID int PRIMARY KEY)" - ).fetchall() + connection_test.query(f"CREATE TABLE {schema}.test_table (oldID int PRIMARY KEY)").fetchall() mySchema = dj.VirtualModule(schema, schema) assert ( - "oldID" - not in mySchema.TestTable.proj(new_name="oldID").heading.attributes.keys() + "oldID" not in mySchema.TestTable.proj(new_name="oldID").heading.attributes.keys() ), "Failed to rename attribute correctly" connection_test.query(f"DROP TABLE {schema}.test_table") @@ -210,9 +177,7 @@ def test_rename_non_dj_attribute( def test_union(schema_simp_pop): x = set(zip(*IJ.fetch("i", "j"))) y = set(zip(*JI.fetch("i", "j"))) - assert ( - len(x) > 0 and len(y) > 0 and len(IJ() * JI()) < len(x) - ) # ensure the IJ and JI are non-trivial + assert len(x) > 0 and len(y) > 0 and len(IJ() * JI()) < len(x) # ensure the IJ and JI are non-trivial z = set(zip(*(IJ + JI).fetch("i", "j"))) # union assert x.union(y) == z assert len(IJ + JI) == len(z) @@ -242,13 +207,9 @@ def test_preview(schema_simp_pop): def test_heading_repr(schema_simp_pop): x = A * D s = repr(x.heading) - assert len( - list( - 1 - for g in s.split("\n") - if g.strip() and not g.strip().startswith(("-", "#")) - ) - ) == len(x.heading.attributes) + assert len(list(1 for g in s.split("\n") if g.strip() and not g.strip().startswith(("-", "#")))) == len( + x.heading.attributes + ) def test_aggregate(schema_simp_pop): @@ -258,9 +219,7 @@ def test_aggregate(schema_simp_pop): x = B().aggregate(B.C(), keep_all_rows=True) assert len(x) == len(B()) # test LEFT join - assert len((x & "id_b=0").fetch()) == len( - B() & "id_b=0" - ) # test restricted aggregation + assert len((x & "id_b=0").fetch()) == len(B() & "id_b=0") # test restricted aggregation x = B().aggregate( B.C(), @@ -279,12 +238,8 @@ def test_aggregate(schema_simp_pop): values = (B.C() & key).fetch("value") assert bool(len(values)) == bool(n), "aggregation failed (restriction)" if n: - assert np.isclose( - mean, values.mean(), rtol=1e-4, atol=1e-5 - ), "aggregation failed (mean)" - assert np.isclose( - max_, values.max(), rtol=1e-4, atol=1e-5 - ), "aggregation failed (max)" + assert np.isclose(mean, values.mean(), rtol=1e-4, atol=1e-5), "aggregation failed (mean)" + assert np.isclose(max_, values.max(), rtol=1e-4, atol=1e-5), "aggregation failed (max)" def test_aggr(schema_simp_pop): @@ -296,9 +251,7 @@ def test_aggr(schema_simp_pop): x = B().aggr(B.C(), keep_all_rows=True) assert len(x) == len(B()) # test LEFT join - assert len((x & "id_b=0").fetch()) == len( - B() & "id_b=0" - ) # test restricted aggregation + assert len((x & "id_b=0").fetch()) == len(B() & "id_b=0") # test restricted aggregation x = B().aggr( B.C(), @@ -317,12 +270,8 @@ def test_aggr(schema_simp_pop): values = (B.C() & key).fetch("value") assert bool(len(values)) == bool(n), "aggregation failed (restriction)" if n: - assert np.isclose( - mean, values.mean(), rtol=1e-4, atol=1e-5 - ), "aggregation failed (mean)" - assert np.isclose( - max_, values.max(), rtol=1e-4, atol=1e-5 - ), "aggregation failed (max)" + assert np.isclose(mean, values.mean(), rtol=1e-4, atol=1e-5), "aggregation failed (mean)" + assert np.isclose(max_, values.max(), rtol=1e-4, atol=1e-5), "aggregation failed (max)" def test_semijoin(schema_simp_pop): @@ -383,19 +332,14 @@ def test_restrictions_by_lists(schema_simp_pop): assert len(x - set()) == lenx, "incorrect restriction by an empty set" assert len(x & {}) == lenx, "incorrect restriction by a tuple with no attributes" assert len(x - {}) == 0, "incorrect restriction by a tuple with no attributes" - assert ( - len(x & {"foo": 0}) == lenx - ), "incorrect restriction by a tuple with no matching attributes" - assert ( - len(x - {"foo": 0}) == 0 - ), "incorrect restriction by a tuple with no matching attributes" + assert len(x & {"foo": 0}) == lenx, "incorrect restriction by a tuple with no matching attributes" + assert len(x - {"foo": 0}) == 0, "incorrect restriction by a tuple with no matching attributes" assert len(x & y) == len(x & y.fetch()), "incorrect restriction by a list" assert len(x - y) == len(x - y.fetch()), "incorrect restriction by a list" w = A() assert len(w) > 0, "incorrect test setup: w is empty" assert ( - bool(set(w.heading.names) & set(y.heading.names)) - != "incorrect test setup: w and y should have no common attributes" + bool(set(w.heading.names) & set(y.heading.names)) != "incorrect test setup: w and y should have no common attributes" ) assert len(w) == len(w & y), "incorrect restriction without common attributes" assert len(w - y) == 0, "incorrect restriction without common attributes" @@ -429,9 +373,7 @@ def test_date(schema_simp_pop): def test_join_project(schema_simp_pop): """Test join of projected relations with matching non-primary key""" q = DataA.proj() * DataB.proj() - assert ( - len(q) == len(DataA()) == len(DataB()) - ), "Join of projected relations does not work" + assert len(q) == len(DataA()) == len(DataB()), "Join of projected relations does not work" def test_ellipsis(schema_any_pop): @@ -442,9 +384,7 @@ def test_ellipsis(schema_any_pop): def test_update_single_key(schema_simp_pop): """Test that only one row can be updated""" with pytest.raises(dj.DataJointError): - TTestUpdate.update1( - dict(TTestUpdate.fetch1("KEY"), string_attr="my new string") - ) + TTestUpdate.update1(dict(TTestUpdate.fetch1("KEY"), string_attr="my new string")) def test_update_no_primary(schema_simp_pop): @@ -462,9 +402,7 @@ def test_update_missing_attribute(schema_simp_pop): def test_update_string_attribute(schema_simp_pop): """Test replacing a string value""" rel = TTestUpdate() & dict(primary_key=0) - s = "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(10) - ) + s = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) TTestUpdate.update1(dict(rel.fetch1("KEY"), string_attr=s)) assert s == rel.fetch1("string_attr"), "Updated string does not match" @@ -490,9 +428,7 @@ def test_update_blob_attribute(schema_simp_pop): def test_reserved_words(schema_simp_pop): """Test the user of SQL reserved words as attributes""" rel = ReservedWord() - rel.insert1( - {"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"} - ) + rel.insert1({"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"}) assert (rel & {"key": 1, "in": "ouch", "from": "bummer"}).fetch1("int") == 3 assert (rel.proj("int", double="from") & {"double": "bummer"}).fetch1("int") == 3 (rel & {"key": 1}).delete() @@ -501,13 +437,9 @@ def test_reserved_words(schema_simp_pop): def test_reserved_words2(schema_simp_pop): """Test the user of SQL reserved words as attributes""" rel = ReservedWord() - rel.insert1( - {"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"} - ) + rel.insert1({"key": 1, "in": "ouch", "from": "bummer", "int": 3, "select": "major pain"}) with pytest.raises(dj.DataJointError): - (rel & "key=1").fetch( - "in" - ) # error because reserved word `key` is not in backquotes. See issue #249 + (rel & "key=1").fetch("in") # error because reserved word `key` is not in backquotes. See issue #249 def test_permissive_join_basic(schema_any_pop): @@ -560,9 +492,7 @@ def test_joins_with_aggregation(schema_any_pop): SessionA * SessionStatusA & 'status="trained_1a" or status="trained_1b"', date_trained="min(date(session_start_time))", ) - session_dates = ( - SessionDateA * (subj_query & 'date_trained<"2020-12-21"') - ) & "session_date 0 def test_insecure_connection(db_creds_test, connection_test): - result = ( - dj.conn(use_tls=False, reset=True, **db_creds_test) - .query("SHOW STATUS LIKE 'Ssl_cipher';") - .fetchone()[1] - ) + result = dj.conn(use_tls=False, reset=True, **db_creds_test).query("SHOW STATUS LIKE 'Ssl_cipher';").fetchone()[1] assert result == "" diff --git a/tests/test_university.py b/tests/test_university.py index 24f01dd4c..ec2ee6cdd 100644 --- a/tests/test_university.py +++ b/tests/test_university.py @@ -37,9 +37,7 @@ def schema_uni_inactive(): @pytest.fixture def schema_uni(db_creds_test, schema_uni_inactive, connection_test, prefix): # Deferred activation - schema_uni_inactive.activate( - prefix + "_university", connection=dj.conn(**db_creds_test) - ) + schema_uni_inactive.activate(prefix + "_university", connection=dj.conn(**db_creds_test)) # --------------- Fill University ------------------- test_data_dir = Path(__file__).parent / "data" for table in ( @@ -62,9 +60,7 @@ def schema_uni(db_creds_test, schema_uni_inactive, connection_test, prefix): def test_activate_unauthorized(schema_uni_inactive, db_creds_test, connection_test): with pytest.raises(DataJointError): - schema_uni_inactive.activate( - "unauthorized", connection=dj.conn(**db_creds_test) - ) + schema_uni_inactive.activate("unauthorized", connection=dj.conn(**db_creds_test)) def test_fill(schema_uni): @@ -87,22 +83,14 @@ def test_restrict(schema_uni): assert len(utahns1) == len(utahns2.fetch("KEY")) == 7 # male nonutahns - sex1, state1 = ((Student & 'sex="M"') - {"home_state": "UT"}).fetch( - "sex", "home_state", order_by="student_id" - ) - sex2, state2 = ((Student & 'sex="M"') - {"home_state": "UT"}).fetch( - "sex", "home_state", order_by="student_id" - ) + sex1, state1 = ((Student & 'sex="M"') - {"home_state": "UT"}).fetch("sex", "home_state", order_by="student_id") + sex2, state2 = ((Student & 'sex="M"') - {"home_state": "UT"}).fetch("sex", "home_state", order_by="student_id") assert len(set(state1)) == len(set(state2)) == 44 assert set(sex1).pop() == set(sex2).pop() == "M" # students from OK, NM, TX - s1 = (Student & [{"home_state": s} for s in ("OK", "NM", "TX")]).fetch( - "KEY", order_by="student_id" - ) - s2 = (Student & 'home_state in ("OK", "NM", "TX")').fetch( - "KEY", order_by="student_id" - ) + s1 = (Student & [{"home_state": s} for s in ("OK", "NM", "TX")]).fetch("KEY", order_by="student_id") + s2 = (Student & 'home_state in ("OK", "NM", "TX")').fetch("KEY", order_by="student_id") assert len(s1) == 11 assert s1 == s2 @@ -139,34 +127,24 @@ def test_union(schema_uni): def test_aggr(schema_uni): - avg_grade_per_course = Course.aggr( - Grade * LetterGrade, avg_grade="round(avg(points), 2)" - ) + avg_grade_per_course = Course.aggr(Grade * LetterGrade, avg_grade="round(avg(points), 2)") assert len(avg_grade_per_course) == 45 # GPA - student_gpa = Student.aggr( - Course * Grade * LetterGrade, gpa="round(sum(points*credits)/sum(credits), 2)" - ) + student_gpa = Student.aggr(Course * Grade * LetterGrade, gpa="round(sum(points*credits)/sum(credits), 2)") gpa = student_gpa.fetch("gpa") assert len(gpa) == 261 assert 2 < gpa.mean() < 3 # Sections in biology department with zero students in them - section = (Section & {"dept": "BIOL"}).aggr( - Enroll, n="count(student_id)", keep_all_rows=True - ) & "n=0" + section = (Section & {"dept": "BIOL"}).aggr(Enroll, n="count(student_id)", keep_all_rows=True) & "n=0" assert len(set(section.fetch("dept"))) == 1 assert len(section) == 17 assert bool(section) # Test correct use of ellipses in a similar query - section = (Section & {"dept": "BIOL"}).aggr( - Grade, ..., n="count(student_id)", keep_all_rows=True - ) & "n>1" - assert not any( - name in section.heading.names for name in Grade.heading.secondary_attributes - ) + section = (Section & {"dept": "BIOL"}).aggr(Grade, ..., n="count(student_id)", keep_all_rows=True) & "n>1" + assert not any(name in section.heading.names for name in Grade.heading.secondary_attributes) assert len(set(section.fetch("dept"))) == 1 assert len(section) == 168 assert bool(section) diff --git a/tests/test_update1.py b/tests/test_update1.py index ff53466d4..fcae3335c 100644 --- a/tests/test_update1.py +++ b/tests/test_update1.py @@ -1,5 +1,4 @@ import os -import tempfile from pathlib import Path import numpy as np @@ -27,9 +26,7 @@ def mock_stores_update(tmpdir_factory): og_stores_config = dj.config.get("stores") if "stores" not in dj.config: dj.config["stores"] = {} - dj.config["stores"]["update_store"] = dict( - protocol="file", location=tmpdir_factory.mktemp("store") - ) + dj.config["stores"]["update_store"] = dict(protocol="file", location=tmpdir_factory.mktemp("store")) dj.config["stores"]["update_repo"] = dict( stage=tmpdir_factory.mktemp("repo_stage"), protocol="file", @@ -44,9 +41,7 @@ def mock_stores_update(tmpdir_factory): @pytest.fixture def schema_update1(connection_test, prefix): - schema = dj.Schema( - prefix + "_update1", context=dict(Thing=Thing), connection=connection_test - ) + schema = dj.Schema(prefix + "_update1", context=dict(Thing=Thing), connection=connection_test) schema(Thing) yield schema schema.drop() @@ -99,9 +94,7 @@ def test_update1(tmpdir, enable_filepath_feature, schema_update1, mock_stores_up ) check3 = Thing.fetch1() - assert ( - check1["number"] == 0 and check1["picture"] is None and check1["params"] is None - ) + assert check1["number"] == 0 and check1["picture"] is None and check1["params"] is None assert ( check2["number"] == 3 @@ -124,9 +117,7 @@ def test_update1(tmpdir, enable_filepath_feature, schema_update1, mock_stores_up assert original_file_data == final_file_data -def test_update1_nonexistent( - enable_filepath_feature, schema_update1, mock_stores_update -): +def test_update1_nonexistent(enable_filepath_feature, schema_update1, mock_stores_update): with pytest.raises(DataJointError): # updating a non-existent entry Thing.update1(dict(thing=100, frac=0.5)) @@ -138,9 +129,7 @@ def test_update1_noprimary(enable_filepath_feature, schema_update1, mock_stores_ Thing.update1(dict(number=None)) -def test_update1_misspelled_attribute( - enable_filepath_feature, schema_update1, mock_stores_update -): +def test_update1_misspelled_attribute(enable_filepath_feature, schema_update1, mock_stores_update): key = dict(thing=17) Thing.insert1(dict(key, frac=1.5)) with pytest.raises(DataJointError): From de4ce275e7863cc876331d8588cfbbd6a62dcf1e Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 21:05:54 +0200 Subject: [PATCH 015/219] update linting workflow --- .github/workflows/lint.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 62468a983..e7e6dc2ae 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -23,7 +23,7 @@ jobs: extra_args: codespell --all-files - uses: pre-commit/action@v3.0.1 with: - extra_args: black --all-files + extra_args: ruff --all-files - uses: pre-commit/action@v3.0.1 with: - extra_args: flake8 --all-files + extra_args: ruff-format --all-files From 59d0159cc13d65b454f1e276151fd7a2eb9280d5 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 21:06:12 +0200 Subject: [PATCH 016/219] update test workflow to use src layout --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 196ddec22..e1e28a1c8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,7 @@ on: - "!gh-pages" # exclude gh-pages branch - "!stage*" # exclude branches beginning with stage paths: - - "datajoint" + - "src/datajoint" - "tests" pull_request: branches: @@ -14,7 +14,7 @@ on: - "!gh-pages" # exclude gh-pages branch - "!stage*" # exclude branches beginning with stage paths: - - "datajoint" + - "src/datajoint" - "tests" jobs: test: From 85ff04168b0bdf592804696d44bf2375da30f09b Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 21:06:12 +0200 Subject: [PATCH 017/219] update test workflow to use src layout --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 196ddec22..e1e28a1c8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -6,7 +6,7 @@ on: - "!gh-pages" # exclude gh-pages branch - "!stage*" # exclude branches beginning with stage paths: - - "datajoint" + - "src/datajoint" - "tests" pull_request: branches: @@ -14,7 +14,7 @@ on: - "!gh-pages" # exclude gh-pages branch - "!stage*" # exclude branches beginning with stage paths: - - "datajoint" + - "src/datajoint" - "tests" jobs: test: From 2007f335df8fc67a08e306aee43dead67f5ba3e6 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Tue, 16 Sep 2025 23:13:02 +0200 Subject: [PATCH 018/219] update hook invocations to use src layout --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a58e0483..ccf72ed80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: rev: 25.1.0 # matching versions in pyproject.toml and github actions hooks: - id: black - args: ["--check", "-v", "datajoint", "tests", "--diff"] # --required-version is conflicting with pre-commit + args: ["--check", "-v", "src", "tests", "--diff"] # --required-version is conflicting with pre-commit - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: @@ -41,7 +41,7 @@ repos: - --count - --show-source - --statistics - files: datajoint # a lot of files in tests are not compliant + files: src/ # a lot of files in tests are not compliant # style tests - id: flake8 args: @@ -51,7 +51,7 @@ repos: - --max-line-length=127 - --statistics - --per-file-ignores=datajoint/diagram.py:C901 - files: datajoint # a lot of files in tests are not compliant + files: src/ # a lot of files in tests are not compliant - repo: https://github.com/rhysd/actionlint rev: v1.7.7 hooks: From b3b712b128279cfc7b0c1f6a802591273df34d88 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 10 Nov 2025 07:10:11 +0100 Subject: [PATCH 019/219] simplify devcontainer --- .devcontainer/devcontainer.json | 61 ++++----------------------------- 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6ed3c52c4..f7a442c87 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,56 +1,7 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose { - "name": "Existing Docker Compose (Extend)", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. - "dockerComposeFile": [ - "../docker-compose.yaml", - "docker-compose.yml" - ], - // The 'service' property is the name of the service for the container that VS Code should - // use. Update this value and .devcontainer/docker-compose.yml to the real service name. - "service": "app", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - // Features to add to the dev container. More info: https://containers.dev/features. - // "features": {}, - // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [ - 80, - 443, - 3306, - 8080, - 9000 - ], - "mounts": [ - "type=bind,source=${env:SSH_AUTH_SOCK},target=/ssh-agent" - ], - "containerEnv": { - "SSH_AUTH_SOCK": "/ssh-agent" - }, - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - "shutdownAction": "stopCompose", - "onCreateCommand": "python3 -m pip install -q -e .[dev]", - "features": { - "ghcr.io/devcontainers/features/git:1": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, - "ghcr.io/devcontainers/features/github-cli:1": {}, - }, - // Configure tool-specific properties. - "customizations": { - "vscode": { - "extensions": [ - "ms-python.python" - ] - } - }, - "remoteEnv": { - "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}" - } - // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "devcontainer" -} + "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "postCreateCommand": "curl -fsSL https://pixi.sh/install.sh | bash && echo 'export PATH=\"$HOME/.pixi/bin:$PATH\"' >> ~/.bashrc" +} \ No newline at end of file From a506d400627480958a5fa2d7850e16ce8df776ff Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 10 Nov 2025 07:10:37 +0100 Subject: [PATCH 020/219] update deps, and add activate script for dot --- activate.sh | 4 + pixi.lock | 3629 ++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 8 +- 3 files changed, 3517 insertions(+), 124 deletions(-) create mode 100644 activate.sh diff --git a/activate.sh b/activate.sh new file mode 100644 index 000000000..1632accc8 --- /dev/null +++ b/activate.sh @@ -0,0 +1,4 @@ +#! /usr/bin/bash +# This script registers dot plugins so that we can use graphviz +# to write png images +dot -c \ No newline at end of file diff --git a/pixi.lock b/pixi.lock index 6a7cd0202..e823b4f84 100644 --- a/pixi.lock +++ b/pixi.lock @@ -145,6 +145,261 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl - pypi: ./ + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-atk-2.38.0-h1f2db35_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-core-2.40.3-h1f2db35_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/atk-1.0-2.38.0-hedc4a1f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/epoxy-1.5.10-he30d5cf_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.1-h8af1aa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.4-h90308e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/glib-tools-2.86.1-hc87f4d4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphviz-13.1.2-hdb06ba2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gtk3-3.24.43-h4cd1324_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gts-0.7.6-he293c15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-12.2.0-he4899c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/hicolor-icon-theme-0.17-h8af1aa0_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-hd32f0e1_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcups-2.3.3-h5cdc715_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-hd65408f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.1-h8af1aa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.1-hdae7a39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-he277a41_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgd-2.3.3-hc8d7b1d_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.1-he84ff74_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-he277a41_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.60.0-h8171147_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.0-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-h3f4de04_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hf1166c9_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.2-h3e4203c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.13.0-h3c6a4c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-16-2.15.1-h8591a01_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.15.1-h788dabe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.5.4-h8e36d6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.9-h4c0d347_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h4f8a99f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.46-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcomposite-0.4.6-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdamage-1.1.6-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxi-1.8.2-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxinerama-1.1.5-h5ad3122_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrandr-1.5.4-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxtst-1.2.5-h57736b2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: ./ + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.4-h7542897_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.86.1-hb9d6e3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h5febe37_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.1.0-haf38c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.4-hf598326_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.1-he69a767_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.9-hfc2f54d_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: ./ dev: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -314,6 +569,309 @@ environments: - pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl - pypi: ./ + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-atk-2.38.0-h1f2db35_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-core-2.40.3-h1f2db35_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/atk-1.0-2.38.0-hedc4a1f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/epoxy-1.5.10-he30d5cf_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.1-h8af1aa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.4-h90308e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/glib-tools-2.86.1-hc87f4d4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphviz-13.1.2-hdb06ba2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gtk3-3.24.43-h4cd1324_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gts-0.7.6-he293c15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-12.2.0-he4899c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/hicolor-icon-theme-0.17-h8af1aa0_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-hd32f0e1_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcups-2.3.3-h5cdc715_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-hd65408f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.1-h8af1aa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.1-hdae7a39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-he277a41_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgd-2.3.3-hc8d7b1d_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.1-he84ff74_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-he277a41_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.60.0-h8171147_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.0-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-h3f4de04_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hf1166c9_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.2-h3e4203c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.13.0-h3c6a4c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-16-2.15.1-h8591a01_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.15.1-h788dabe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.5.4-h8e36d6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.9-h4c0d347_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h4f8a99f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.46-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcomposite-0.4.6-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdamage-1.1.6-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxi-1.8.2-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxinerama-1.1.5-h5ad3122_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrandr-1.5.4-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxtst-1.2.5-h57736b2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: ./ + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.4-h7542897_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.86.1-hb9d6e3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h5febe37_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.1.0-haf38c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.4-hf598326_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.1-he69a767_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.9-hfc2f54d_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: ./ test: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -470,80 +1028,412 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl - pypi: ./ -packages: -- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 - md5: d7c89558ba9fa0495403155b64376d81 - license: None - purls: [] - size: 2562 - timestamp: 1578324546067 -- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - build_number: 16 - sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 - md5: 73aaf86a425cc6e73fcf236a5a46396d - depends: - - _libgcc_mutex 0.1 conda_forge - - libgomp >=7.5.0 - constrains: - - openmp_impl 9999 - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 23621 - timestamp: 1650670423406 -- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_1.conda - sha256: f52307d3ff839bf4a001cb14b3944f169e46e37982a97c3d52cbf48a0cfe2327 - md5: 388097ca1f27fc28e0ef1986dd311891 - depends: - - __unix - - hicolor-icon-theme - - librsvg - license: LGPL-3.0-or-later OR CC-BY-SA-3.0 - license_family: LGPL - purls: [] - size: 621553 - timestamp: 1755882037787 -- pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - name: argon2-cffi - version: 25.1.0 - sha256: fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 - requires_dist: - - argon2-cffi-bindings - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - name: argon2-cffi-bindings - version: 25.1.0 - sha256: d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a - requires_dist: - - cffi>=1.0.1 ; python_full_version < '3.14' - - cffi>=2.0.0b1 ; python_full_version >= '3.14' - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - name: asttokens - version: 3.0.0 - sha256: e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2 - requires_dist: - - astroid>=2,<4 ; extra == 'astroid' - - astroid>=2,<4 ; extra == 'test' - - pytest ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-xdist ; extra == 'test' - requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 - sha256: 26ab9386e80bf196e51ebe005da77d57decf6d989b4f34d96130560bc133479c - md5: 6b889f174df1e0f816276ae69281af4d - depends: - - at-spi2-core >=2.40.0,<2.41.0a0 - - atk-1.0 >=2.36.0 - - dbus >=1.13.6,<2.0a0 - - libgcc-ng >=9.3.0 - - libglib >=2.68.1,<3.0a0 - license: LGPL-2.1-or-later - license_family: LGPL - purls: [] - size: 339899 - timestamp: 1619122953439 + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-atk-2.38.0-h1f2db35_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-core-2.40.3-h1f2db35_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/atk-1.0-2.38.0-hedc4a1f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/epoxy-1.5.10-he30d5cf_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.1-h8af1aa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.4-h90308e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/glib-tools-2.86.1-hc87f4d4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphviz-13.1.2-hdb06ba2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gtk3-3.24.43-h4cd1324_6.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gts-0.7.6-he293c15_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-12.2.0-he4899c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/hicolor-icon-theme-0.17-h8af1aa0_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-hd32f0e1_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcups-2.3.3-h5cdc715_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-hd65408f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.1-h8af1aa0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.1-hdae7a39_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-he277a41_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgd-2.3.3-hc8d7b1d_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.1-he84ff74_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-devel-1.7.0-hd24410f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-he277a41_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.60.0-h8171147_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.0-h022381a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-h3f4de04_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hf1166c9_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.2-h3e4203c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.13.0-h3c6a4c8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-16-2.15.1-h8591a01_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.15.1-h788dabe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.5.4-h8e36d6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.9-h4c0d347_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h4f8a99f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.46-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcomposite-0.4.6-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdamage-1.1.6-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxi-1.8.2-h57736b2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxinerama-1.1.5-h5ad3122_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrandr-1.5.4-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxtst-1.2.5-h57736b2_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: ./ + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.4-h7542897_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.86.1-hb9d6e3a_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h5febe37_6.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.1.0-haf38c7b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.4-hf598326_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.1-he69a767_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.9-hfc2f54d_101_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/e6/efe534ef0952b531b630780e19cabd416e2032697019d5295defc6ef9bd9/deepdiff-8.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + - pypi: ./ +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: 3702bef2f0a4d38bd8288bbe54aace623602a1343c2cfbefd3fa188e015bebf0 + md5: 6168d71addc746e8f2b8d57dfd2edcea + depends: + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23712 + timestamp: 1650670790230 +- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_1.conda + sha256: f52307d3ff839bf4a001cb14b3944f169e46e37982a97c3d52cbf48a0cfe2327 + md5: 388097ca1f27fc28e0ef1986dd311891 + depends: + - __unix + - hicolor-icon-theme + - librsvg + license: LGPL-3.0-or-later OR CC-BY-SA-3.0 + license_family: LGPL + purls: [] + size: 621553 + timestamp: 1755882037787 +- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda + sha256: a362b4f5c96a0bf4def96be1a77317e2730af38915eb9bec85e2a92836501ed7 + md5: b3f0179590f3c0637b7eb5309898f79e + depends: + - __unix + - hicolor-icon-theme + - librsvg + license: LGPL-3.0-or-later OR CC-BY-SA-3.0 + license_family: LGPL + purls: [] + size: 631452 + timestamp: 1758743294412 +- pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl + name: argon2-cffi + version: 25.1.0 + sha256: fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + requires_dist: + - argon2-cffi-bindings + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: 7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: argon2-cffi-bindings + version: 25.1.0 + sha256: 1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 + requires_dist: + - cffi>=1.0.1 ; python_full_version < '3.14' + - cffi>=2.0.0b1 ; python_full_version >= '3.14' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl + name: asttokens + version: 3.0.0 + sha256: e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2 + requires_dist: + - astroid>=2,<4 ; extra == 'astroid' + - astroid>=2,<4 ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-xdist ; extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + sha256: 26ab9386e80bf196e51ebe005da77d57decf6d989b4f34d96130560bc133479c + md5: 6b889f174df1e0f816276ae69281af4d + depends: + - at-spi2-core >=2.40.0,<2.41.0a0 + - atk-1.0 >=2.36.0 + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.1,<3.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 339899 + timestamp: 1619122953439 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-atk-2.38.0-h1f2db35_3.tar.bz2 + sha256: c2c2c998d49c061e390537f929e77ce6b023ef22b51a0f55692d6df7327f3358 + md5: 4ea9d4634f3b054549be5e414291801e + depends: + - at-spi2-core >=2.40.0,<2.41.0a0 + - atk-1.0 >=2.36.0 + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.1,<3.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 322172 + timestamp: 1619123713021 - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 sha256: c4f9b66bd94c40d8f1ce1fad2d8b46534bdefda0c86e3337b28f6c25779f258d md5: 8cb2fc4cd6cc63f1369cfa318f581cc3 @@ -559,6 +1449,21 @@ packages: purls: [] size: 658390 timestamp: 1625848454791 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/at-spi2-core-2.40.3-h1f2db35_0.tar.bz2 + sha256: cd48de9674a20133e70a643476accc1a63360c921ab49477638364877937a40d + md5: a12602a94ee402b57063ef74e82016c0 + depends: + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.3,<3.0a0 + - xorg-libx11 + - xorg-libxi + - xorg-libxtst + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 622407 + timestamp: 1625848355776 - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda sha256: df682395d05050cd1222740a42a551281210726a67447e5258968dd55854302e md5: f730d54ba9cd543666d7220c9f7ed563 @@ -573,6 +1478,35 @@ packages: purls: [] size: 355900 timestamp: 1713896169874 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/atk-1.0-2.38.0-hedc4a1f_2.conda + sha256: 69f70048a1a915be7b8ad5d2cbb7bf020baa989b5506e45a676ef4ef5106c4f0 + md5: 9308557e2328f944bd5809c5630761af + depends: + - libgcc-ng >=12 + - libglib >=2.80.0,<3.0a0 + - libstdcxx-ng >=12 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 358327 + timestamp: 1713898303194 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + sha256: b0747f9b1bc03d1932b4d8c586f39a35ac97e7e72fe6e63f2b2a2472d466f3c1 + md5: 57301986d02d30d6805fdce6c99074ee + depends: + - __osx >=11.0 + - libcxx >=16 + - libglib >=2.80.0,<3.0a0 + - libintl >=0.22.5,<1.0a0 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 347530 + timestamp: 1713896411580 - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl name: black version: 24.2.0 @@ -603,6 +1537,35 @@ packages: purls: [] size: 260341 timestamp: 1757437258798 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_8.conda + sha256: d2a296aa0b5f38ed9c264def6cf775c0ccb0f110ae156fcde322f3eccebf2e01 + md5: 2921ac0b541bf37c69e66bd6d9a43bca + depends: + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 192536 + timestamp: 1757437302703 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_8.conda + sha256: b456200636bd5fecb2bec63f7e0985ad2097cf1b83d60ce0b6968dffa6d02aa1 + md5: 58fd217444c2a5701a44244faf518206 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 125061 + timestamp: 1757437486465 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.10.5-hbd8a1cb_0.conda + sha256: 3b5ad78b8bb61b6cdc0978a6a99f8dfb2cc789a451378d054698441005ecbdb6 + md5: f9e5fbc24009179e8b0409624691758a + depends: + - __unix + license: ISC + purls: [] + size: 155907 + timestamp: 1759649036195 - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.8.3-hbd8a1cb_0.conda sha256: 837b795a2bb39b75694ba910c13c15fa4998d4bb2a622c214a6a5174b2ae53d1 md5: 74784ee3d225fc3dca89edb635b4e5cc @@ -638,11 +1601,67 @@ packages: purls: [] size: 978114 timestamp: 1741554591855 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/cairo-1.18.4-h83712da_0.conda + sha256: 37cfff940d2d02259afdab75eb2dbac42cf830adadee78d3733d160a1de2cc66 + md5: cd55953a67ec727db5dc32b167201aa6 + depends: + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.5,<2.0a0 + - xorg-libx11 >=1.8.11,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + size: 966667 + timestamp: 1741554768968 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + sha256: 00439d69bdd94eaf51656fdf479e0c853278439d22ae151cabf40eb17399d95f + md5: 38f6df8bc8c668417b904369a01ba2e2 + depends: + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=18 + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + size: 896173 + timestamp: 1741554795915 - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl name: certifi version: 2025.8.3 sha256: f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl + name: certifi + version: 2025.10.5 + sha256: 0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + name: cffi + version: 2.0.0 + sha256: 45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: cffi version: 2.0.0 @@ -650,6 +1669,13 @@ packages: requires_dist: - pycparser ; implementation_name != 'PyPy' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: cffi + version: 2.0.0 + sha256: d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl name: cfgv version: 3.4.0 @@ -660,6 +1686,16 @@ packages: version: 3.4.3 sha256: 416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: charset-normalizer + version: 3.4.4 + sha256: 6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl + name: charset-normalizer + version: 3.4.4 + sha256: e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl name: click version: 8.2.1 @@ -667,6 +1703,13 @@ packages: requires_dist: - colorama ; sys_platform == 'win32' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl + name: click + version: 8.3.0 + sha256: 9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl name: codespell version: 2.4.1 @@ -715,6 +1758,56 @@ packages: - pytest-xdist ; extra == 'test-no-images' - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: contourpy + version: 1.3.3 + sha256: 348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl + name: contourpy + version: 1.3.3 + sha256: d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1 + requires_dist: + - numpy>=1.25 + - furo ; extra == 'docs' + - sphinx>=7.2 ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - bokeh ; extra == 'bokeh' + - selenium ; extra == 'bokeh' + - contourpy[bokeh,docs] ; extra == 'mypy' + - bokeh ; extra == 'mypy' + - docutils-stubs ; extra == 'mypy' + - mypy==1.17.0 ; extra == 'mypy' + - types-pillow ; extra == 'mypy' + - contourpy[test-no-images] ; extra == 'test' + - matplotlib ; extra == 'test' + - pillow ; extra == 'test' + - pytest ; extra == 'test-no-images' + - pytest-cov ; extra == 'test-no-images' + - pytest-rerunfailures ; extra == 'test-no-images' + - pytest-xdist ; extra == 'test-no-images' + - wurlitzer ; extra == 'test-no-images' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl name: coverage version: 7.10.6 @@ -722,6 +1815,20 @@ packages: requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl + name: coverage + version: 7.11.0 + sha256: f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: coverage + version: 7.11.2 + sha256: 811bff1f93566a8556a9aeb078bd82573e37f4d802a185fba4cbe75468615050 + requires_dist: + - tomli ; python_full_version <= '3.11' and extra == 'toml' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl name: cycler version: 0.12.1 @@ -738,7 +1845,7 @@ packages: - pypi: ./ name: datajoint version: 0.14.6 - sha256: 649c71b2cbfb0b38be5fe9a421035a7001e815777208d50274a7a31986c40e91 + sha256: 8da2585511ca6906c53e2fe4ecec75250c274eed754f2902bf5b69767ea006da requires_dist: - numpy - pymysql>=0.7.2 @@ -761,7 +1868,7 @@ packages: - codespell ; extra == 'dev' - pytest ; extra == 'dev' - pytest-cov ; extra == 'dev' - requires_python: '>=3.9,<4.0' + requires_python: '>=3.9,<3.14' editable: true - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068 @@ -779,6 +1886,21 @@ packages: purls: [] size: 437860 timestamp: 1747855126005 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/dbus-1.16.2-heda779d_0.conda + sha256: 5c9166bbbe1ea7d0685a1549aad4ea887b1eb3a07e752389f86b185ef8eac99a + md5: 9203b74bb1f3fa0d6f308094b3b44c1e + depends: + - libgcc >=13 + - libstdcxx >=13 + - libgcc >=13 + - libexpat >=2.7.0,<3.0a0 + - libglib >=2.84.2,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 469781 + timestamp: 1747855172617 - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl name: decorator version: 5.2.1 @@ -850,6 +1972,38 @@ packages: purls: [] size: 1440699 timestamp: 1648505042260 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/epoxy-1.5.10-he30d5cf_2.conda + sha256: aa562cdd72d2d15b0f2ee4565c8e34f18b52f7135a3f3b1ce727c202425c3bec + md5: 1c50e7c46ccefffe918ac974fa1a6752 + depends: + - libdrm >=2.4.125,<2.5.0a0 + - libegl >=1.7.0,<2.0a0 + - libegl-devel + - libgcc >=14 + - libgl >=1.7.0,<2.0a0 + - libgl-devel + - libglx >=1.7.0,<2.0a0 + - libglx-devel + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxdamage >=1.1.6,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxxf86vm >=1.1.6,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 422103 + timestamp: 1758743388115 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-hc919400_2.conda + sha256: ba685b87529c95a4bf9de140a33d703d57dc46b036e9586ed26890de65c1c0d5 + md5: 3b87dabebe54c6d66a07b97b53ac5874 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 296347 + timestamp: 1758743805063 - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl name: executing version: 2.2.1 @@ -870,11 +2024,23 @@ packages: requires_dist: - tzdata requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl + name: faker + version: 37.12.0 + sha256: afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4 + requires_dist: + - tzdata + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl name: filelock version: 3.19.1 sha256: d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl + name: filelock + version: 3.20.0 + sha256: 339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl name: flake8 version: 7.3.0 @@ -931,6 +2097,33 @@ packages: purls: [] size: 265599 timestamp: 1730283881107 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fontconfig-2.15.0-h8dda3cd_1.conda + sha256: fe023bb8917c8a3138af86ef537b70c8c5d60c44f93946a87d1e8bb1a6634b55 + md5: 112b71b6af28b47c624bcbeefeea685b + depends: + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 277832 + timestamp: 1730284967179 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + sha256: f79d3d816fafbd6a2b0f75ebc3251a30d3294b08af9bb747194121f5efa364bc + md5: 7b29f48742cea5d1ccb5edd839cb5621 + depends: + - __osx >=11.0 + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 234227 + timestamp: 1730284037572 - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 md5: fee5683a3f04bd15cbd8318b096a27ab @@ -954,6 +2147,19 @@ packages: purls: [] size: 4102 timestamp: 1566932280397 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + sha256: 54eea8469786bc2291cc40bca5f46438d3e062a399e8f53f013b6a9f50e98333 + md5: a7970cd949a077b7cb9696379d338681 + depends: + - font-ttf-ubuntu + - font-ttf-inconsolata + - font-ttf-dejavu-sans-mono + - font-ttf-source-code-pro + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4059 + timestamp: 1762351264405 - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl name: fonttools version: 4.59.2 @@ -988,6 +2194,74 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.23.0 ; extra == 'all' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: fonttools + version: 4.60.1 + sha256: 2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.23.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.23.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl + name: fonttools + version: 4.60.1 + sha256: 6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb + requires_dist: + - lxml>=4.0 ; extra == 'lxml' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'woff' + - zopfli>=0.1.4 ; extra == 'woff' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'unicode' + - lz4>=1.7.4.2 ; extra == 'graphite' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'interpolatable' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'interpolatable' + - pycairo ; extra == 'interpolatable' + - matplotlib ; extra == 'plot' + - sympy ; extra == 'symfont' + - xattr ; sys_platform == 'darwin' and extra == 'type1' + - skia-pathops>=0.5.0 ; extra == 'pathops' + - uharfbuzz>=0.23.0 ; extra == 'repacker' + - lxml>=4.0 ; extra == 'all' + - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'all' + - brotlicffi>=0.8.0 ; platform_python_implementation != 'CPython' and extra == 'all' + - zopfli>=0.1.4 ; extra == 'all' + - unicodedata2>=15.1.0 ; python_full_version < '3.13' and extra == 'all' + - lz4>=1.7.4.2 ; extra == 'all' + - scipy ; platform_python_implementation != 'PyPy' and extra == 'all' + - munkres ; platform_python_implementation == 'PyPy' and extra == 'all' + - pycairo ; extra == 'all' + - matplotlib ; extra == 'all' + - sympy ; extra == 'all' + - xattr ; sys_platform == 'darwin' and extra == 'all' + - skia-pathops>=0.5.0 ; extra == 'all' + - uharfbuzz>=0.23.0 ; extra == 'all' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.1-ha770c72_0.conda sha256: bf8e4dffe46f7d25dc06f31038cacb01672c47b9f45201f065b0f4d00ab0a83e md5: 4afc585cd97ba8a23809406cd8a9eda8 @@ -998,6 +2272,26 @@ packages: purls: [] size: 173114 timestamp: 1757945422243 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.14.1-h8af1aa0_0.conda + sha256: 9f8de35e95ce301cecfe01bc9d539c7cc045146ffba55efe9733ff77ad1cfb21 + md5: 0c8f36ebd3678eed1685f0fc93fc2175 + depends: + - libfreetype 2.14.1 h8af1aa0_0 + - libfreetype6 2.14.1 hdae7a39_0 + license: GPL-2.0-only OR FTL + purls: [] + size: 173174 + timestamp: 1757945489158 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.1-hce30654_0.conda + sha256: 14427aecd72e973a73d5f9dfd0e40b6bc3791d253de09b7bf233f6a9a190fd17 + md5: 1ec9a1ee7a2c9339774ad9bb6fe6caec + depends: + - libfreetype 2.14.1 hce30654_0 + - libfreetype6 2.14.1 h6da58f4_0 + license: GPL-2.0-only OR FTL + purls: [] + size: 173399 + timestamp: 1757947175403 - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.16-hb03c661_0.conda sha256: 858283ff33d4c033f4971bf440cebff217d5552a5222ba994c49be990dacd40d md5: f9f81ea472684d75b9dd8d0b328cf655 @@ -1008,6 +2302,24 @@ packages: purls: [] size: 61244 timestamp: 1757438574066 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/fribidi-1.0.16-he30d5cf_0.conda + sha256: 1bfcd715bcb49a0b22d5d1899a22c6ff884b06f8e141eb746f3949752469a422 + md5: f3ac54914f7d3e1d68cb8d891765e5f9 + depends: + - libgcc >=14 + license: LGPL-2.1-or-later + purls: [] + size: 62909 + timestamp: 1757438620177 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + sha256: d856dc6744ecfba78c5f7df3378f03a75c911aadac803fa2b41a583667b4b600 + md5: 04bdce8d93a4ed181d1d726163c2d447 + depends: + - __osx >=11.0 + license: LGPL-2.1-or-later + purls: [] + size: 59391 + timestamp: 1757438897523 - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.44.1-h2b0a6b4_0.conda sha256: b827285fe001806beeddcc30953d2bd07869aeb0efe4581d56432c92c06b0c48 md5: 2935d9c0526277bd42373cf23d49d51f @@ -1024,6 +2336,37 @@ packages: purls: [] size: 579596 timestamp: 1757867209855 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gdk-pixbuf-2.44.4-h90308e0_0.conda + sha256: 78a1d69c3d0da73b4d54a35001abd4e273605180d21365b4f31e9a241d9fb715 + md5: 4c8c0d2f7620467869d41f29304362dc + depends: + - libgcc >=14 + - libglib >=2.86.0,<3.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 580454 + timestamp: 1761083738779 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.44.4-h7542897_0.conda + sha256: 1164ba63360736439c6e50f2d390e93f04df86901e7711de41072a32d9b8bfc9 + md5: 0b349c0400357e701cf2fa69371e5d39 + depends: + - __osx >=11.0 + - libglib >=2.86.0,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libpng >=1.6.50,<1.7.0a0 + - libtiff >=4.7.1,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 544149 + timestamp: 1761082904334 - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.86.0-hf516916_0.conda sha256: b77316bd5c8680bde4e5a7ab7013c8f0f10c1702cc6c3b0fd0fac3923a31fec3 md5: 1a8e49615381c381659de1bc6a3bf9ec @@ -1035,18 +2378,61 @@ packages: purls: [] size: 117284 timestamp: 1757403341964 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/glib-tools-2.86.1-hc87f4d4_1.conda + sha256: 59d89ed84223775b4354c2bc0fc51c465ee1caf53607bf7eae868b0aca4b5a9e + md5: eabd2c76bb4cbf80fd78bb5e7d8122d7 + depends: + - libgcc >=14 + - libglib 2.86.1 he84ff74_1 + license: LGPL-2.1-or-later + purls: [] + size: 126254 + timestamp: 1761874152194 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.86.1-hb9d6e3a_1.conda + sha256: 6492472d76db47d85699c895acbe6b578ee0d4a964490388e71aec8777c0e9ec + md5: 5a90e74e57c0d1e2381ce1246b0a2125 + depends: + - __osx >=11.0 + - libglib 2.86.1 he69a767_1 + - libintl >=0.25.1,<1.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 101419 + timestamp: 1761875708283 - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-hecca717_2.conda sha256: 25ba37da5c39697a77fce2c9a15e48cf0a84f1464ad2aafbe53d8357a9f6cc8c md5: 2cd94587f3a401ae05e03a6caf09539d depends: - - __glibc >=2.17,<3.0.a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 99596 + timestamp: 1755102025473 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphite2-1.3.14-hfae3067_2.conda + sha256: c9b1781fe329e0b77c5addd741e58600f50bef39321cae75eba72f2f381374b7 + md5: 4aa540e9541cc9d6581ab23ff2043f13 + depends: - libgcc >=14 - libstdcxx >=14 license: LGPL-2.0-or-later license_family: LGPL purls: [] - size: 99596 - timestamp: 1755102025473 + size: 102400 + timestamp: 1755102000043 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-hec049ff_2.conda + sha256: c507ae9989dbea7024aa6feaebb16cbf271faac67ac3f0342ef1ab747c20475d + md5: 0fc46fee39e88bbcf5835f71a9d9a209 + depends: + - __osx >=11.0 + - libcxx >=19 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 81202 + timestamp: 1755102333712 - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl name: graphviz version: '0.21' @@ -1092,6 +2478,54 @@ packages: purls: [] size: 2427887 timestamp: 1754732581595 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/graphviz-13.1.2-hdb06ba2_0.conda + sha256: 15f0f8bc5b5fc1c51be13f0dd4e2dcfb4cd6555e75b18656d51def0d8b7e4db2 + md5: 52fc4ad5de8b211077edfa9e657f6cab + depends: + - adwaita-icon-theme + - cairo >=1.18.4,<2.0a0 + - fonts-conda-ecosystem + - gdk-pixbuf >=2.42.12,<3.0a0 + - gtk3 >=3.24.43,<4.0a0 + - gts >=0.7.6,<0.8.0a0 + - libexpat >=2.7.1,<3.0a0 + - libgcc >=14 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.84.3,<3.0a0 + - librsvg >=2.58.4,<3.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: EPL-1.0 + license_family: Other + purls: [] + size: 2557826 + timestamp: 1754732391605 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.2-hcd33d8b_0.conda + sha256: f25e1828d02ebd78214966f483cfca5ac6a7b18824369c748d8cda99c66ff588 + md5: 81ab85a5a8481667660c7ce6e84bd681 + depends: + - __osx >=11.0 + - adwaita-icon-theme + - cairo >=1.18.4,<2.0a0 + - fonts-conda-ecosystem + - gdk-pixbuf >=2.42.12,<3.0a0 + - gtk3 >=3.24.43,<4.0a0 + - gts >=0.7.6,<0.8.0a0 + - libcxx >=19 + - libexpat >=2.7.1,<3.0a0 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.84.3,<3.0a0 + - librsvg >=2.58.4,<3.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: EPL-1.0 + license_family: Other + purls: [] + size: 2201370 + timestamp: 1754732518951 - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda sha256: d36263cbcbce34ec463ce92bd72efa198b55d987959eab6210cc256a0e79573b md5: 67d00e9cfe751cfe581726c5eff7c184 @@ -1133,6 +2567,70 @@ packages: purls: [] size: 5585389 timestamp: 1743405684985 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gtk3-3.24.43-h4cd1324_6.conda + sha256: 5b8c5255d88d97083095790765dfafda6ce99daa8dcaaa8c0b668e82c5b73187 + md5: 124842a6e0b59cbd121233346bd56e33 + depends: + - at-spi2-atk >=2.38.0,<3.0a0 + - atk-1.0 >=2.38.0 + - cairo >=1.18.4,<2.0a0 + - epoxy >=1.5.10,<1.6.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.16,<2.0a0 + - gdk-pixbuf >=2.44.4,<3.0a0 + - glib-tools + - harfbuzz >=11.5.1 + - hicolor-icon-theme + - libcups >=2.3.3,<2.4.0a0 + - libcups >=2.3.3,<3.0a0 + - libexpat >=2.7.1,<3.0a0 + - libgcc >=14 + - libglib >=2.86.0,<3.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxkbcommon >=1.12.2,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + - wayland >=1.24.0,<2.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxcomposite >=0.4.6,<1.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - xorg-libxdamage >=1.1.6,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.2,<7.0a0 + - xorg-libxi >=1.8.2,<2.0a0 + - xorg-libxinerama >=1.1.5,<1.2.0a0 + - xorg-libxrandr >=1.5.4,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 5660172 + timestamp: 1761334356772 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h5febe37_6.conda + sha256: bd66a3325bf3ce63ada3bf12eaafcfe036698741ee4bb595e83e5fdd3dba9f3d + md5: a99f96906158ebae5e3c0904bcd45145 + depends: + - __osx >=11.0 + - atk-1.0 >=2.38.0 + - cairo >=1.18.4,<2.0a0 + - epoxy >=1.5.10,<1.6.0a0 + - fribidi >=1.0.16,<2.0a0 + - gdk-pixbuf >=2.44.4,<3.0a0 + - glib-tools + - harfbuzz >=11.5.1 + - hicolor-icon-theme + - libexpat >=2.7.1,<3.0a0 + - libglib >=2.86.0,<3.0a0 + - libintl >=0.25.1,<1.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 4768791 + timestamp: 1761328318680 - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda sha256: b5cd16262fefb836f69dc26d879b6508d29f8a5c5948a966c47fe99e2e19c99b md5: 4d8df0b0db060d33c9a702ada998a8fe @@ -1145,6 +2643,29 @@ packages: purls: [] size: 318312 timestamp: 1686545244763 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gts-0.7.6-he293c15_4.conda + sha256: 1e9cc30d1c746d5a3399a279f5f642a953f37d9f9c82fd4d55b301e9c2a23f7c + md5: 2aeaeddbd89e84b60165463225814cfc + depends: + - libgcc-ng >=12 + - libglib >=2.76.3,<3.0a0 + - libstdcxx-ng >=12 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 332673 + timestamp: 1686545222091 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + sha256: e0f8c7bc1b9ea62ded78ffa848e37771eeaaaf55b3146580513c7266862043ba + md5: 21b4dd3098f63a74cf2aa9159cbef57d + depends: + - libcxx >=15.0.7 + - libglib >=2.76.3,<3.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 304331 + timestamp: 1686545503242 - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.5.0-h15599e2_0.conda sha256: 04d33cef3345ce6e3fbbfb5539ebc8a3730026ea94ce6ace1f8f8d3551fa079c md5: 47599428437d622bfee24fbd06a2d0b4 @@ -1164,6 +2685,44 @@ packages: purls: [] size: 2048134 timestamp: 1757867460348 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/harfbuzz-12.2.0-he4899c9_0.conda + sha256: 5cfd74a3fbce0921af5beff93a3fe7edc5b1344d9b9668b2de1c1be932b54993 + md5: 1437bf9690976948f90175a65407b65f + depends: + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libgcc >=14 + - libglib >=2.86.1,<3.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 2156041 + timestamp: 1762376447693 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-12.1.0-haf38c7b_0.conda + sha256: 8f2fac3e74608af55334ab9e77e9db9112c9078858aa938d191481d873a902d3 + md5: 3fd0b257d246ddedd1f1496e5246958d + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - graphite2 >=1.3.14,<2.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=19 + - libexpat >=2.7.1,<3.0a0 + - libfreetype >=2.14.1 + - libfreetype6 >=2.14.1 + - libglib >=2.86.0,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1548996 + timestamp: 1759366687572 - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 sha256: 336f29ceea9594f15cc8ec4c45fdc29e10796573c697ee0d57ebb7edd7e92043 md5: bbf6f174dcd3254e19a2f5d2295ce808 @@ -1172,6 +2731,22 @@ packages: purls: [] size: 13841 timestamp: 1605162808667 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/hicolor-icon-theme-0.17-h8af1aa0_2.tar.bz2 + sha256: 479a0f95cf3e7d7db795fb7a14337cab73c2c926a5599c8512a3e8f8466f9e54 + md5: 331add9f855e921695d7b569aa23d5ec + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 13896 + timestamp: 1605162856037 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + sha256: 286e33fb452f61133a3a61d002890235d1d1378554218ab063d6870416440281 + md5: 237b05b7eb284d7eebc3c5d93f5e4bca + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 13800 + timestamp: 1611053664863 - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e md5: 8b189310083baabfb622af68fd9d3ae3 @@ -1184,6 +2759,27 @@ packages: purls: [] size: 12129203 timestamp: 1720853576813 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-75.1-hf9b3779_0.conda + sha256: 813298f2e54ef087dbfc9cc2e56e08ded41de65cff34c639cc8ba4e27e4540c9 + md5: 268203e8b983fddb6412b36f2024e75c + depends: + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + purls: [] + size: 12282786 + timestamp: 1720853454991 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 + md5: 5eb22c1d7b3fc4abb50d92d621583137 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 11857802 + timestamp: 1720853997952 - pypi: https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl name: identify version: 2.6.14 @@ -1191,6 +2787,13 @@ packages: requires_dist: - ukkonen ; extra == 'license' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl + name: identify + version: 2.6.15 + sha256: 1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 + requires_dist: + - ukkonen ; extra == 'license' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl name: idna version: '3.10' @@ -1201,11 +2804,26 @@ packages: - pytest>=8.3.2 ; extra == 'all' - flake8>=7.1.1 ; extra == 'all' requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + name: idna + version: '3.11' + sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + - flake8>=7.1.1 ; extra == 'all' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl name: iniconfig version: 2.1.0 sha256: 9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + name: iniconfig + version: 2.3.0 + sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl name: ipython version: 9.5.0 @@ -1251,6 +2869,95 @@ packages: - matplotlib ; extra == 'matplotlib' - ipython[doc,matplotlib,test,test-extra] ; extra == 'all' requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl + name: ipython + version: 9.6.0 + sha256: 5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196 + requires_dist: + - colorama ; sys_platform == 'win32' + - decorator + - ipython-pygments-lexers + - jedi>=0.16 + - matplotlib-inline + - pexpect>4.3 ; sys_platform != 'emscripten' and sys_platform != 'win32' + - prompt-toolkit>=3.0.41,<3.1.0 + - pygments>=2.4.0 + - stack-data + - traitlets>=5.13.0 + - typing-extensions>=4.6 ; python_full_version < '3.12' + - black ; extra == 'black' + - docrepr ; extra == 'doc' + - exceptiongroup ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - ipykernel ; extra == 'doc' + - ipython[matplotlib,test] ; extra == 'doc' + - setuptools>=61.2 ; extra == 'doc' + - sphinx-toml==0.0.4 ; extra == 'doc' + - sphinx-rtd-theme ; extra == 'doc' + - sphinx>=1.3 ; extra == 'doc' + - typing-extensions ; extra == 'doc' + - pytest ; extra == 'test' + - pytest-asyncio ; extra == 'test' + - testpath ; extra == 'test' + - packaging ; extra == 'test' + - ipython[test] ; extra == 'test-extra' + - curio ; extra == 'test-extra' + - jupyter-ai ; extra == 'test-extra' + - ipython[matplotlib] ; extra == 'test-extra' + - nbformat ; extra == 'test-extra' + - nbclient ; extra == 'test-extra' + - ipykernel ; extra == 'test-extra' + - numpy>=1.25 ; extra == 'test-extra' + - pandas>2.0 ; extra == 'test-extra' + - trio ; extra == 'test-extra' + - matplotlib>3.7 ; extra == 'matplotlib' + - ipython[doc,matplotlib,test,test-extra] ; extra == 'all' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl + name: ipython + version: 9.7.0 + sha256: bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f + requires_dist: + - colorama>=0.4.4 ; sys_platform == 'win32' + - decorator>=4.3.2 + - ipython-pygments-lexers>=1.0.0 + - jedi>=0.18.1 + - matplotlib-inline>=0.1.5 + - pexpect>4.3 ; sys_platform != 'emscripten' and sys_platform != 'win32' + - prompt-toolkit>=3.0.41,<3.1.0 + - pygments>=2.11.0 + - stack-data>=0.6.0 + - traitlets>=5.13.0 + - typing-extensions>=4.6 ; python_full_version < '3.12' + - black ; extra == 'black' + - docrepr ; extra == 'doc' + - exceptiongroup ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - ipykernel ; extra == 'doc' + - ipython[matplotlib,test] ; extra == 'doc' + - setuptools>=70.0 ; extra == 'doc' + - sphinx-toml==0.0.4 ; extra == 'doc' + - sphinx-rtd-theme>=0.1.8 ; extra == 'doc' + - sphinx>=8.0 ; extra == 'doc' + - typing-extensions ; extra == 'doc' + - pytest>=7.0.0 ; extra == 'test' + - pytest-asyncio>=1.0.0 ; extra == 'test' + - testpath>=0.2 ; extra == 'test' + - packaging>=20.1.0 ; extra == 'test' + - setuptools>=61.2 ; extra == 'test' + - ipython[test] ; extra == 'test-extra' + - curio ; extra == 'test-extra' + - jupyter-ai ; extra == 'test-extra' + - ipython[matplotlib] ; extra == 'test-extra' + - nbformat ; extra == 'test-extra' + - nbclient ; extra == 'test-extra' + - ipykernel>6.30 ; extra == 'test-extra' + - numpy>=1.27 ; extra == 'test-extra' + - pandas>2.1 ; extra == 'test-extra' + - trio>=0.1.0 ; extra == 'test-extra' + - matplotlib>3.9 ; extra == 'matplotlib' + - ipython[doc,matplotlib,test,test-extra] ; extra == 'all' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl name: ipython-pygments-lexers version: 1.1.1 @@ -1266,6 +2973,14 @@ packages: - colorama ; extra == 'colors' - setuptools ; extra == 'plugins' requires_python: '>=3.9.0' +- pypi: https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl + name: isort + version: 7.0.0 + sha256: 1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 + requires_dist: + - colorama ; extra == 'colors' + - setuptools ; extra == 'plugins' + requires_python: '>=3.10.0' - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl name: jedi version: 0.19.2 @@ -1316,6 +3031,25 @@ packages: purls: [] size: 134088 timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda + sha256: 5ce830ca274b67de11a7075430a72020c1fb7d486161a82839be15c2b84e9988 + md5: e7df0aab10b9cbb73ab2a467ebfaf8c7 + depends: + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + size: 129048 + timestamp: 1754906002667 +- pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl + name: kiwisolver + version: 1.4.9 + sha256: 1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + name: kiwisolver + version: 1.4.9 + sha256: 5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: kiwisolver version: 1.4.9 @@ -1336,6 +3070,21 @@ packages: purls: [] size: 1370023 timestamp: 1719463201255 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda + sha256: 0ec272afcf7ea7fbf007e07a3b4678384b7da4047348107b2ae02630a570a815 + md5: 29c10432a2ca1472b53f299ffb2ffa37 + depends: + - keyutils >=1.6.1,<2.0a0 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1474620 + timestamp: 1719463205834 - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 md5: 0be7c6e070c19105f966d3758448d018 @@ -1348,6 +3097,17 @@ packages: purls: [] size: 676044 timestamp: 1752032747103 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.44-hd32f0e1_5.conda + sha256: cc03f3e2d5d48f1193a2d0822971b085d583327d6e20f2a5cf7d030ffdb35f9a + md5: 7c87c0b72575b30626a6dc5b49229f0c + depends: + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-aarch64 2.44 + license: GPL-3.0-only + purls: [] + size: 782949 + timestamp: 1762674873740 - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda sha256: 412381a43d5ff9bbed82cd52a0bbca5b90623f62e41007c9c42d3870c60945ff md5: 9344155d33912347b37f0ae6c410a835 @@ -1360,11 +3120,46 @@ packages: purls: [] size: 264243 timestamp: 1745264221534 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-hfdc4d58_1.conda + sha256: f01df5bbf97783fac9b89be602b4d02f94353f5221acfd80c424ec1c9a8d276c + md5: 60dceb7e876f4d74a9cbd42bbbc6b9cf + depends: + - libgcc >=13 + - libstdcxx >=13 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 227184 + timestamp: 1745265544057 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + sha256: 12361697f8ffc9968907d1a7b5830e34c670e4a59b638117a2cdfed8f63a38f8 + md5: a74332d9b60b62905e3d30709df08bf1 + depends: + - __osx >=11.0 + - libcxx >=18 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 188306 + timestamp: 1745264362794 - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda sha256: cb83980c57e311783ee831832eb2c20ecb41e7dee6e86e8b70b8cef0e43eab55 md5: d4a250da4737ee127fb1fa6452a9002e depends: - - __glibc >=2.17,<3.0.a0 + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 4523621 + timestamp: 1749905341688 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libcups-2.3.3-h5cdc715_5.conda + sha256: f3282d27be35e5d29b5b798e5136427ec798916ee6374499be7b7682c8582b72 + md5: ac0333d338076ef19170938bbaf97582 + depends: - krb5 >=1.21.3,<1.22.0a0 - libgcc >=13 - libstdcxx >=13 @@ -1372,8 +3167,18 @@ packages: license: Apache-2.0 license_family: Apache purls: [] - size: 4523621 - timestamp: 1749905341688 + size: 4550533 + timestamp: 1749906839681 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-21.1.4-hf598326_2.conda + sha256: 0a0765cc8b6000e7f7be879c12825583d046ef22ab95efc7c5f8622e4b3302d5 + md5: 4346830dcc0c0e930328fddb0b829f63 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 568742 + timestamp: 1761852287381 - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda sha256: 8420748ea1cc5f18ecc5068b4f24c7a023cc9b20971c99c824ba10641fb95ddf md5: 64f0c503da58ec25ebd359e4d990afa8 @@ -1385,6 +3190,37 @@ packages: purls: [] size: 72573 timestamp: 1747040452262 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.25-h1af38f5_0.conda + sha256: 48814b73bd462da6eed2e697e30c060ae16af21e9fbed30d64feaf0aad9da392 + md5: a9138815598fe6b91a1d6782ca657b0c + depends: + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 71117 + timestamp: 1761979776756 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + sha256: 417d52b19c679e1881cce3f01cad3a2d542098fa2d6df5485aac40f01aede4d1 + md5: 3baf58a5a87e7c2f4d243ce2f8f2fe5c + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 54790 + timestamp: 1747040549847 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libdrm-2.4.125-he30d5cf_1.conda + sha256: 4e6cdb5dd37db794b88bec714b4418a0435b04d14e9f7afc8cc32f2a3ced12f2 + md5: 2079727b538f6dd16f3fa579d4c3c53f + depends: + - libgcc >=14 + - libpciaccess >=0.18,<0.19.0a0 + license: MIT + license_family: MIT + purls: [] + size: 344548 + timestamp: 1757212128414 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 md5: c277e0a4d549b03ac1e9d6cbbe3d017b @@ -1398,6 +3234,38 @@ packages: purls: [] size: 134676 timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20250104-pl5321h976ea20_0.conda + sha256: c0b27546aa3a23d47919226b3a1635fccdb4f24b94e72e206a751b33f46fd8d6 + md5: fb640d776fc92b682a14e001980825b1 + depends: + - ncurses + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 148125 + timestamp: 1738479808948 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-1.7.0-hd24410f_2.conda + sha256: 8962abf38a58c235611ce356b9899f6caeb0352a8bce631b0bcc59352fda455e + md5: cf105bce884e4ef8c8ccdca9fe6695e7 + depends: + - libglvnd 1.7.0 hd24410f_2 + license: LicenseRef-libglvnd + purls: [] + size: 53551 + timestamp: 1731330990477 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libegl-devel-1.7.0-hd24410f_2.conda + sha256: 9c8e9d2289316741d037f0c5003de42488780d181453543f75497dd5a4891c7c + md5: cd8877e3833ba1bfac2fbaa5ae72c226 + depends: + - libegl 1.7.0 hd24410f_2 + - libgl-devel 1.7.0 hd24410f_2 + - xorg-libx11 + license: LicenseRef-libglvnd + purls: [] + size: 30397 + timestamp: 1731331017398 - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.1-hecca717_0.conda sha256: da2080da8f0288b95dd86765c801c6e166c4619b910b11f9a8446fb852438dc2 md5: 4211416ecba1866fab0c6470986c22d6 @@ -1411,6 +3279,30 @@ packages: purls: [] size: 74811 timestamp: 1752719572741 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.1-hfae3067_0.conda + sha256: 378cabff44ea83ce4d9f9c59f47faa8d822561d39166608b3e65d1e06c927415 + md5: f75d19f3755461db2eb69401f5514f4c + depends: + - libgcc >=14 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 74309 + timestamp: 1752719762749 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.1-hec049ff_0.conda + sha256: 8fbb17a56f51e7113ed511c5787e0dec0d4b10ef9df921c4fd1cccca0458f648 + md5: b1ca5f21335782f71a8bd69bdc093f67 + depends: + - __osx >=11.0 + constrains: + - expat 2.7.1.* + license: MIT + license_family: MIT + purls: [] + size: 65971 + timestamp: 1752719657566 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab md5: ede4673863426c0883c0063d853bbd85 @@ -1422,6 +3314,26 @@ packages: purls: [] size: 57433 timestamp: 1743434498161 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-hd65408f_0.conda + sha256: 6c3332e78a975e092e54f87771611db81dcb5515a3847a3641021621de76caea + md5: 0c5ad486dcfb188885e3cf8ba209b97b + depends: + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 55586 + timestamp: 1760295405021 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-he5f378a_0.conda + sha256: 9b8acdf42df61b7bfe8bdc545c016c29e61985e79748c64ad66df47dbc2e295f + md5: 411ff7cd5d1472bba0f55c0faf04453b + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40251 + timestamp: 1760295839166 - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.1-ha770c72_0.conda sha256: 4641d37faeb97cf8a121efafd6afd040904d4bca8c46798122f417c31d5dfbec md5: f4084e4e6577797150f9b04a4560ceb0 @@ -1431,6 +3343,24 @@ packages: purls: [] size: 7664 timestamp: 1757945417134 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype-2.14.1-h8af1aa0_0.conda + sha256: 342c07e4be3d09d04b531c889182a11a488e7e9ba4b75f642040e4681c1e9b98 + md5: 1e61fb236ccd3d6ccaf9e91cb2d7e12d + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 7753 + timestamp: 1757945484817 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.1-hce30654_0.conda + sha256: 9de25a86066f078822d8dd95a83048d7dc2897d5d655c0e04a8a54fca13ef1ef + md5: f35fb38e89e2776994131fbf961fa44b + depends: + - libfreetype6 >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 7810 + timestamp: 1757947168537 - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.1-h73754d4_0.conda sha256: 4a7af818a3179fafb6c91111752954e29d3a2a950259c14a2fc7ba40a8b03652 md5: 8e7251989bca326a28f4a5ffbd74557a @@ -1445,6 +3375,32 @@ packages: purls: [] size: 386739 timestamp: 1757945416744 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libfreetype6-2.14.1-hdae7a39_0.conda + sha256: cedc83d9733363aca353872c3bfed2e188aa7caf57b57842ba0c6d2765652b7c + md5: 9c2f56b6e011c6d8010ff43b796aab2f + depends: + - libgcc >=14 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 423210 + timestamp: 1757945484108 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.1-h6da58f4_0.conda + sha256: cc4aec4c490123c0f248c1acd1aeab592afb6a44b1536734e20937cda748f7cd + md5: 6d4ede03e2a8e20eb51f7f681d2a2550 + depends: + - __osx >=11.0 + - libpng >=1.6.50,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.14.1 + license: GPL-2.0-only OR FTL + purls: [] + size: 346703 + timestamp: 1757947166116 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_5.conda sha256: 0caed73aac3966bfbf5710e06c728a24c6c138605121a3dacb2e03440e8baa6a md5: 264fbfba7fb20acf3b29cde153e345ce @@ -1459,6 +3415,19 @@ packages: purls: [] size: 824191 timestamp: 1757042543820 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-he277a41_7.conda + sha256: 616f5960930ad45b48c57f49c3adddefd9423674b331887ef0e69437798c214b + md5: afa05d91f8d57dd30985827a09c21464 + depends: + - _openmp_mutex >=4.5 + constrains: + - libgomp 15.2.0 he277a41_7 + - libgcc-ng ==15.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 510719 + timestamp: 1759967448307 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_5.conda sha256: f54bb9c3be12b24be327f4c1afccc2969712e0b091cdfbd1d763fb3e61cda03f md5: 069afdf8ea72504e48d23ae1171d951c @@ -1469,6 +3438,16 @@ packages: purls: [] size: 29187 timestamp: 1757042549554 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-15.2.0-he9431aa_7.conda + sha256: 7d98979b2b5698330007b0146b8b4b95b3790378de12129ce13c9fc88c1ef45a + md5: a5ce1f0a32f02c75c11580c5b2f9258a + depends: + - libgcc 15.2.0 he277a41_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29261 + timestamp: 1759967452303 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda sha256: 19e5be91445db119152217e8e8eec4fd0499d854acc7d8062044fb55a70971cd md5: 68fc66282364981589ef36868b1a7c78 @@ -1490,6 +3469,67 @@ packages: purls: [] size: 177082 timestamp: 1737548051015 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgd-2.3.3-hc8d7b1d_11.conda + sha256: 7e199bb390f985b34aee38cdb1f0d166abc09ed44bd703a1b91a3c6cd9912d45 + md5: d256b0311b7a207a2c6b68d2b399f707 + depends: + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.45,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GD + license_family: BSD + purls: [] + size: 191033 + timestamp: 1737548098172 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + sha256: be038eb8dfe296509aee2df21184c72cb76285b0340448525664bc396aa6146d + md5: 4581aa3cfcd1a90967ed02d4a9f3db4b + depends: + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libiconv >=1.17,<2.0a0 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.45,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GD + license_family: BSD + purls: [] + size: 156868 + timestamp: 1737548290283 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-1.7.0-hd24410f_2.conda + sha256: 3e954380f16255d1c8ae5da3bd3044d3576a0e1ac2e3c3ff2fe8f2f1ad2e467a + md5: 0d00176464ebb25af83d40736a2cd3bb + depends: + - libglvnd 1.7.0 hd24410f_2 + - libglx 1.7.0 hd24410f_2 + license: LicenseRef-libglvnd + purls: [] + size: 145442 + timestamp: 1731331005019 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgl-devel-1.7.0-hd24410f_2.conda + sha256: ec5c3125b38295bad8acc80f793b8ee217ccb194338d73858be278db50ea82f1 + md5: 5d8323dff6a93596fb6f985cf6e8521a + depends: + - libgl 1.7.0 hd24410f_2 + - libglx-devel 1.7.0 hd24410f_2 + license: LicenseRef-libglvnd + purls: [] + size: 113925 + timestamp: 1731331014056 - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.86.0-h1fed272_0.conda sha256: 33336bd55981be938f4823db74291e1323454491623de0be61ecbe6cf3a4619c md5: b8e4c93f4ab70c3b6f6499299627dbdc @@ -1506,6 +3546,65 @@ packages: purls: [] size: 3978602 timestamp: 1757403291664 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglib-2.86.1-he84ff74_1.conda + sha256: 5212c30d9e14a9480c7d25bf93ccca4db23d3794430c9be90e13124d9a8b1687 + md5: f0fc1b2fa2e68b1309852e5c3c8e011d + depends: + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.46,<10.47.0a0 + constrains: + - glib 2.86.1 *_1 + license: LGPL-2.1-or-later + purls: [] + size: 4040523 + timestamp: 1761874121589 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.86.1-he69a767_1.conda + sha256: 253ac4eca90006b19571f8c4766e8ebdad0f01f44de1bfa0472d3df9be9c8ac8 + md5: acff031bb5b97602d2b7ef913a8ea076 + depends: + - __osx >=11.0 + - libffi >=3.5.2,<3.6.0a0 + - libiconv >=1.18,<2.0a0 + - libintl >=0.25.1,<1.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.46,<10.47.0a0 + constrains: + - glib 2.86.1 *_1 + license: LGPL-2.1-or-later + purls: [] + size: 3677659 + timestamp: 1761875607047 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglvnd-1.7.0-hd24410f_2.conda + sha256: 57ec3898a923d4bcc064669e90e8abfc4d1d945a13639470ba5f3748bd3090da + md5: 9e115653741810778c9a915a2f8439e7 + license: LicenseRef-libglvnd + purls: [] + size: 152135 + timestamp: 1731330986070 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-1.7.0-hd24410f_2.conda + sha256: 6591af640cb05a399fab47646025f8b1e1a06a0d4bbb4d2e320d6629b47a1c61 + md5: 1d4269e233636148696a67e2d30dad2a + depends: + - libglvnd 1.7.0 hd24410f_2 + - xorg-libx11 >=1.8.9,<2.0a0 + license: LicenseRef-libglvnd + purls: [] + size: 77736 + timestamp: 1731330998960 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libglx-devel-1.7.0-hd24410f_2.conda + sha256: 4bc28ecc38f30ca1ac66a8fb6c5703f4d888381ec46d3938b7c3383210061ec5 + md5: 1f9ddbb175a63401662d1c6222cef6ff + depends: + - libglx 1.7.0 hd24410f_2 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-xorgproto + license: LicenseRef-libglvnd + purls: [] + size: 26362 + timestamp: 1731331008489 - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_5.conda sha256: 125051d51a8c04694d0830f6343af78b556dd88cc249dfec5a97703ebfb1832d md5: dcd5ff1940cd38f6df777cac86819d60 @@ -1516,6 +3615,14 @@ packages: purls: [] size: 447215 timestamp: 1757042483384 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-he277a41_7.conda + sha256: 0a024f1e4796f5d90fb8e8555691dad1b3bdfc6ac3c2cd14d876e30f805fcac7 + md5: 34cef4753287c36441f907d5fdd78d42 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 450308 + timestamp: 1759967379407 - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f md5: 915f5995e94f60e9a4826e0b0920ee88 @@ -1526,6 +3633,34 @@ packages: purls: [] size: 790176 timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.18-h90929bb_2.conda + sha256: 1473451cd282b48d24515795a595801c9b65b567fe399d7e12d50b2d6cdb04d9 + md5: 5a86bf847b9b926f3a4f203339748d78 + depends: + - libgcc >=14 + license: LGPL-2.1-only + purls: [] + size: 791226 + timestamp: 1754910975665 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda + sha256: de0336e800b2af9a40bdd694b03870ac4a848161b35c8a2325704f123f185f03 + md5: 4d5a7445f0b25b6a3ddbb56e790f5251 + depends: + - __osx >=11.0 + license: LGPL-2.1-only + purls: [] + size: 750379 + timestamp: 1754909073836 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + sha256: 99d2cebcd8f84961b86784451b010f5f0a795ed1c08f1e7c76fbb3c22abf021a + md5: 5103f6a6b210a3912faf8d7db516918c + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 90957 + timestamp: 1751558394144 - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda sha256: 98b399287e27768bf79d48faba8a99a2289748c65cd342ca21033fab1860d4a4 md5: 9fa334557db9f63da6c9285fd2a48638 @@ -1538,6 +3673,28 @@ packages: purls: [] size: 628947 timestamp: 1745268527144 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.1.2-he30d5cf_0.conda + sha256: 84064c7c53a64291a585d7215fe95ec42df74203a5bf7615d33d49a3b0f08bb6 + md5: 5109d7f837a3dfdf5c60f60e311b041f + depends: + - libgcc >=14 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 691818 + timestamp: 1762094728337 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + sha256: 78df2574fa6aa5b6f5fc367c03192f8ddf8e27dc23641468d54e031ff560b9d4 + md5: 01caa4fbcaf0e6b08b3aef1151e91745 + depends: + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 553624 + timestamp: 1745268405713 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 md5: 1a580f7796c7bf6393fddb8bbbde58dc @@ -1550,6 +3707,28 @@ packages: purls: [] size: 112894 timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.1-h86ecc28_2.conda + sha256: 498ea4b29155df69d7f20990a7028d75d91dbea24d04b2eb8a3d6ef328806849 + md5: 7d362346a479256857ab338588190da0 + depends: + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 125103 + timestamp: 1749232230009 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 + md5: d6df911d4564d77c4374b02552cb17d1 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 92286 + timestamp: 1749230283517 - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb9d3cd8_0.conda sha256: 3aa92d4074d4063f2a162cd8ecb45dccac93e543e565c01a787e16a43501f7ee md5: c7e925f37e3b40d893459e625f6a53f1 @@ -1561,6 +3740,36 @@ packages: purls: [] size: 91183 timestamp: 1748393666725 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-h86ecc28_0.conda + sha256: ef8697f934c80b347bf9d7ed45650928079e303bad01bd064995b0e3166d6e7a + md5: 78cfed3f76d6f3f279736789d319af76 + depends: + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 114064 + timestamp: 1748393729243 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h5505292_0.conda + sha256: 0a1875fc1642324ebd6c4ac864604f3f18f57fbcf558a8264f6ced028a3c75b2 + md5: 85ccccb47823dd9f7a99d2c7f530342f + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 71829 + timestamp: 1748393749336 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpciaccess-0.18-h86ecc28_0.conda + sha256: 7641dfdfe9bda7069ae94379e9924892f0b6604c1a016a3f76b230433bb280f2 + md5: 5044e160c5306968d956c2a0a2a440d6 + depends: + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 29512 + timestamp: 1749901899881 - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h421ea60_1.conda sha256: e75a2723000ce3a4b9fd9b9b9ce77553556c93e475a4657db6ed01abc02ea347 md5: 7af8e91b0deb5f8e25d1a595dea79614 @@ -1572,6 +3781,26 @@ packages: purls: [] size: 317390 timestamp: 1753879899951 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.50-h1abf092_1.conda + sha256: e1effd7335ec101bb124f41a5f79fabb5e7b858eafe0f2db4401fb90c51505a7 + md5: ed42935ac048d73109163d653d9445a0 + depends: + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + purls: [] + size: 339168 + timestamp: 1753879915462 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h280e0eb_1.conda + sha256: a2e0240fb0c79668047b528976872307ea80cb330baf8bf6624ac2c6443449df + md5: 4d0f5ce02033286551a32208a5519884 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + purls: [] + size: 287056 + timestamp: 1753879907258 - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda sha256: a45ef03e6e700cc6ac6c375e27904531cf8ade27eb3857e080537ff283fb0507 md5: d27665b20bc4d074b86e628b3ba5ab8b @@ -1592,6 +3821,38 @@ packages: purls: [] size: 6543651 timestamp: 1743368725313 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/librsvg-2.60.0-h8171147_0.conda + sha256: b6cb38e95a447a04e624b6070981899e18c03f71915476fe024dadf384f48f15 + md5: 7e4a8318e73ba685615f90bff926bfe4 + depends: + - cairo >=1.18.4,<2.0a0 + - gdk-pixbuf >=2.44.3,<3.0a0 + - libgcc >=14 + - libglib >=2.86.0,<3.0a0 + - libxml2-16 >=2.14.6 + - pango >=1.56.4,<2.0a0 + constrains: + - __glibc >=2.17 + license: LGPL-2.1-or-later + purls: [] + size: 2995492 + timestamp: 1759335330016 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.60.0-h5c55ec3_0.conda + sha256: ca5a2de5d3f68e8d6443ea1bf193c1596a278e6f86018017c0ccd4928eaf8971 + md5: 05ad1d6b6fb3b384f7a07128025725cb + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - gdk-pixbuf >=2.44.3,<3.0a0 + - libglib >=2.86.0,<3.0a0 + - libxml2-16 >=2.14.6 + - pango >=1.56.4,<2.0a0 + constrains: + - __osx >=11.0 + license: LGPL-2.1-or-later + purls: [] + size: 2344343 + timestamp: 1759328503184 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.4-h0c1763c_0.conda sha256: 6d9c32fc369af5a84875725f7ddfbfc2ace795c28f246dc70055a79f9b2003da md5: 0b367fad34931cb79e0d6b7e5c06bb1c @@ -1603,6 +3864,27 @@ packages: purls: [] size: 932581 timestamp: 1753948484112 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.51.0-h022381a_0.conda + sha256: f66a40b6e07a6f8ce6ccbd38d079b7394217d8f8ae0a05efa644aa0a40140671 + md5: 8920ce2226463a3815e2183c8b5008b8 + depends: + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 938476 + timestamp: 1762299829629 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.4-h4237e3c_0.conda + sha256: 802ebe62e6bc59fc26b26276b793e0542cfff2d03c086440aeaf72fb8bbcec44 + md5: 1dcb0468f5146e38fae99aef9656034b + depends: + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 902645 + timestamp: 1753948599139 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_5.conda sha256: 0f5f61cab229b6043541c13538d75ce11bd96fb2db76f94ecf81997b1fde6408 md5: 4e02a49aaa9d5190cb630fa43528fbe6 @@ -1614,6 +3896,18 @@ packages: purls: [] size: 3896432 timestamp: 1757042571458 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-h3f4de04_7.conda + sha256: 4c6d1a2ae58044112233a57103bbf06000bd4c2aad44a0fd3b464b05fa8df514 + md5: 6a2f0ee17851251a85fbebafbe707d2d + depends: + - libgcc 15.2.0 he277a41_7 + constrains: + - libstdcxx-ng ==15.2.0=*_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3831785 + timestamp: 1759967470295 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_5.conda sha256: 7b8cabbf0ab4fe3581ca28fe8ca319f964078578a51dd2ca3f703c1d21ba23ff md5: 8bba50c7f4679f08c861b597ad2bda6b @@ -1624,6 +3918,16 @@ packages: purls: [] size: 29233 timestamp: 1757042603319 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-15.2.0-hf1166c9_7.conda + sha256: 26fc1bdb39042f27302b363785fea6f6b9607f9c2f5eb949c6ae0bdbb8599574 + md5: 9e5deec886ad32f3c6791b3b75c78681 + depends: + - libstdcxx 15.2.0 h3f4de04_7 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29341 + timestamp: 1759967498023 - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-h8261f1e_6.conda sha256: c62694cd117548d810d2803da6d9063f78b1ffbf7367432c5388ce89474e9ebe md5: b6093922931b535a7ba566b6f384fbe6 @@ -1642,6 +3946,40 @@ packages: purls: [] size: 433078 timestamp: 1755011934951 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.7.1-hdb009f0_1.conda + sha256: 7ff79470db39e803e21b8185bc8f19c460666d5557b1378d1b1e857d929c6b39 + md5: 8c6fd84f9c87ac00636007c6131e457d + depends: + - lerc >=4.0.0,<5.0a0 + - libdeflate >=1.25,<1.26.0a0 + - libgcc >=14 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libstdcxx >=14 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 488407 + timestamp: 1762022048105 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.1-h7dc4979_0.conda + sha256: 6bc1b601f0d3ee853acd23884a007ac0a0290f3609dabb05a47fc5a0295e2b53 + md5: 2bb9e04e2da869125e2dc334d665f00d + depends: + - __osx >=11.0 + - lerc >=4.0.0,<5.0a0 + - libcxx >=19 + - libdeflate >=1.24,<1.25.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libwebp-base >=1.6.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 373640 + timestamp: 1758278641520 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.1-he9a06e4_0.conda sha256: 776e28735cee84b97e4d05dd5d67b95221a3e2c09b8b13e3d6dbe6494337d527 md5: af930c65e9a79a3423d6d36e265cef65 @@ -1653,6 +3991,16 @@ packages: purls: [] size: 37087 timestamp: 1757334557450 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.2-h3e4203c_0.conda + sha256: 7aed28ac04e0298bf8f7ad44a23d6f8ee000aa0445807344b16fceedc67cce0f + md5: 3a68e44fdf2a2811672520fdd62996bd + depends: + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 39172 + timestamp: 1758626850999 - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b md5: aea31d2e5b1091feca96fcfe945c3cf9 @@ -1666,6 +4014,30 @@ packages: purls: [] size: 429011 timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.6.0-ha2e29f5_0.conda + sha256: b03700a1f741554e8e5712f9b06dd67e76f5301292958cd3cb1ac8c6fdd9ed25 + md5: 24e92d0942c799db387f5c9d7b81f1af + depends: + - libgcc >=14 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 359496 + timestamp: 1752160685488 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd + md5: e5e7d467f80da752be17796b87fe6385 + depends: + - __osx >=11.0 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 294974 + timestamp: 1752159906788 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa md5: 92ed62436b625154323d40d5f2f11dd7 @@ -1680,6 +4052,19 @@ packages: purls: [] size: 395888 timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.17.0-h262b8f6_0.conda + sha256: 461cab3d5650ac6db73a367de5c8eca50363966e862dcf60181d693236b1ae7b + md5: cd14ee5cca2464a425b1dbfc24d90db2 + depends: + - libgcc >=13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + purls: [] + size: 397493 + timestamp: 1727280745441 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.11.0-he8b52b9_0.conda sha256: 23f47e86cc1386e7f815fa9662ccedae151471862e971ea511c5c886aa723a54 md5: 74e91c36d0eef3557915c68b6c2bef96 @@ -1696,6 +4081,22 @@ packages: purls: [] size: 791328 timestamp: 1754703902365 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxkbcommon-1.13.0-h3c6a4c8_0.conda + sha256: c197e58ba06fa9ac73fcbdc20f9a78ba0164f61879d127bb2f7d0d4be346216a + md5: a7c78be36bf59b4ba44ad2f2f8b92b37 + depends: + - libgcc >=14 + - libstdcxx >=14 + - libxcb >=1.17.0,<2.0a0 + - libxml2 + - libxml2-16 >=2.14.6 + - xkeyboard-config + - xorg-libxau >=1.0.12,<2.0a0 + license: MIT/X11 Derivative + license_family: MIT + purls: [] + size: 862682 + timestamp: 1762341934465 - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h04c0eec_1.conda sha256: 03deb1ec6edfafc5aaeecadfc445ee436fecffcda11fcd97fde9b6632acb583f md5: 10bcbd05e1c1c9d652fccb42b776a9fa @@ -1711,6 +4112,53 @@ packages: purls: [] size: 698448 timestamp: 1754315344761 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.15.1-h788dabe_0.conda + sha256: db0a568e0853ee38b7a4db1cb4ee76e57fe7c32ccb1d5b75f6618a1041d3c6e4 + md5: a0e7779b7625b88e37df9bd73f0638dc + depends: + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2-16 2.15.1 h8591a01_0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 47192 + timestamp: 1761015739999 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-16-2.15.1-h8591a01_0.conda + sha256: 7a13450bce2eeba8f8fb691868b79bf0891377b707493a527bd930d64d9b98af + md5: e7177c6fbbf815da7b215b4cc3e70208 + depends: + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + purls: [] + size: 597078 + timestamp: 1761015734476 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.1-h0ff4647_0.conda + sha256: ebe2dd9da94280ad43da936efa7127d329b559f510670772debc87602b49b06d + md5: 438c97d1e9648dd7342f86049dd44638 + depends: + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + purls: [] + size: 464952 + timestamp: 1761016087733 - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 md5: edb0dca6bc32e4f4789199455a1dbeb8 @@ -1724,6 +4172,30 @@ packages: purls: [] size: 60963 timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + sha256: 5a2c1eeef69342e88a98d1d95bff1603727ab1ff4ee0e421522acd8813439b84 + md5: 08aad7cbe9f5a6b460d0976076b6ae64 + depends: + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 66657 + timestamp: 1727963199518 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: matplotlib version: 3.10.6 @@ -1743,6 +4215,44 @@ packages: - setuptools-scm>=7 ; extra == 'dev' - setuptools>=64 ; extra == 'dev' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl + name: matplotlib + version: 3.10.7 + sha256: 37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: matplotlib + version: 3.10.7 + sha256: 22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632 + requires_dist: + - contourpy>=1.0.1 + - cycler>=0.10 + - fonttools>=4.22.0 + - kiwisolver>=1.3.1 + - numpy>=1.23 + - packaging>=20.0 + - pillow>=8 + - pyparsing>=3 + - python-dateutil>=2.7 + - meson-python>=0.13.1,<0.17.0 ; extra == 'dev' + - pybind11>=2.13.2,!=2.13.3 ; extra == 'dev' + - setuptools-scm>=7 ; extra == 'dev' + - setuptools>=64 ; extra == 'dev' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl name: matplotlib-inline version: 0.1.7 @@ -1750,6 +4260,18 @@ packages: requires_dist: - traitlets requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + name: matplotlib-inline + version: 0.2.1 + sha256: d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76 + requires_dist: + - traitlets + - flake8 ; extra == 'test' + - nbdime ; extra == 'test' + - nbval ; extra == 'test' + - notebook ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl name: mccabe version: 0.7.0 @@ -1766,6 +4288,17 @@ packages: - typing-extensions - urllib3 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl + name: minio + version: 7.2.18 + sha256: f23a6edbff8d0bc4b5c1a61b2628a01c5a3342aefc613ff9c276012e6321108f + requires_dist: + - argon2-cffi + - certifi + - pycryptodome + - typing-extensions + - urllib3 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl name: mypy-extensions version: 1.1.0 @@ -1781,6 +4314,24 @@ packages: purls: [] size: 891641 timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 + md5: 182afabe009dc78d8b73100255ee6868 + depends: + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 926034 + timestamp: 1738196018799 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl name: networkx version: '3.5' @@ -1825,45 +4376,259 @@ packages: - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl name: numpy version: 2.3.3 - sha256: 5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 - requires_python: '>=3.11' -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda - sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e - md5: ffffb341206dd0dab0c36053c048d621 - depends: - - __glibc >=2.17,<3.0.a0 - - ca-certificates - - libgcc >=14 - license: Apache-2.0 - license_family: Apache - purls: [] - size: 3128847 - timestamp: 1754465526100 -- pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl - name: orderly-set - version: 5.5.0 - sha256: 46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7 + sha256: 5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl + name: numpy + version: 2.3.4 + sha256: a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: numpy + version: 2.3.4 + sha256: 4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7 + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.2-h26f9b46_0.conda + sha256: c9f54d4e8212f313be7b02eb962d0cb13a8dae015683a403d3accd4add3e520e + md5: ffffb341206dd0dab0c36053c048d621 + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3128847 + timestamp: 1754465526100 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.5.4-h8e36d6e_0.conda + sha256: a24b318733c98903e2689adc7ef73448e27cbb10806852032c023f0ea4446fc5 + md5: 9303e8887afe539f78517951ce25cd13 + depends: + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3644584 + timestamp: 1759326000128 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.4-h5503f6c_0.conda + sha256: f0512629f9589392c2fb9733d11e753d0eab8fc7602f96e4d7f3bd95c783eb07 + md5: 71118318f37f717eefe55841adb172fd + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3067808 + timestamp: 1759324763146 +- pypi: https://files.pythonhosted.org/packages/12/27/fb8d7338b4d551900fa3e580acbe7a0cf655d940e164cb5c00ec31961094/orderly_set-5.5.0-py3-none-any.whl + name: orderly-set + version: 5.5.0 + sha256: 46f0b801948e98f427b412fcabb831677194c05c3b699b80de260374baa0b1e7 + requires_dist: + - coverage~=7.6.0 ; extra == 'coverage' + - bump2version~=1.0.0 ; extra == 'dev' + - ipdb~=0.13.0 ; extra == 'dev' + - orjson ; extra == 'optimize' + - flake8~=7.1.0 ; extra == 'static' + - flake8-pyproject~=1.2.3 ; extra == 'static' + - pytest~=8.3.0 ; extra == 'test' + - pytest-benchmark~=5.1.0 ; extra == 'test' + - pytest-cov~=6.0.0 ; extra == 'test' + - python-dotenv~=1.0.0 ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl + name: packaging + version: '25.0' + sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pandas + version: 2.3.2 + sha256: 4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b + requires_dist: + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl + name: pandas + version: 2.3.3 + sha256: e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d requires_dist: - - coverage~=7.6.0 ; extra == 'coverage' - - bump2version~=1.0.0 ; extra == 'dev' - - ipdb~=0.13.0 ; extra == 'dev' - - orjson ; extra == 'optimize' - - flake8~=7.1.0 ; extra == 'static' - - flake8-pyproject~=1.2.3 ; extra == 'static' - - pytest~=8.3.0 ; extra == 'test' - - pytest-benchmark~=5.1.0 ; extra == 'test' - - pytest-cov~=6.0.0 ; extra == 'test' - - python-dotenv~=1.0.0 ; extra == 'test' - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - name: packaging - version: '25.0' - sha256: 29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 - requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - numpy>=1.22.4 ; python_full_version < '3.11' + - numpy>=1.23.2 ; python_full_version == '3.11.*' + - numpy>=1.26.0 ; python_full_version >= '3.12' + - python-dateutil>=2.8.2 + - pytz>=2020.1 + - tzdata>=2022.7 + - hypothesis>=6.46.1 ; extra == 'test' + - pytest>=7.3.2 ; extra == 'test' + - pytest-xdist>=2.2.0 ; extra == 'test' + - pyarrow>=10.0.1 ; extra == 'pyarrow' + - bottleneck>=1.3.6 ; extra == 'performance' + - numba>=0.56.4 ; extra == 'performance' + - numexpr>=2.8.4 ; extra == 'performance' + - scipy>=1.10.0 ; extra == 'computation' + - xarray>=2022.12.0 ; extra == 'computation' + - fsspec>=2022.11.0 ; extra == 'fss' + - s3fs>=2022.11.0 ; extra == 'aws' + - gcsfs>=2022.11.0 ; extra == 'gcp' + - pandas-gbq>=0.19.0 ; extra == 'gcp' + - odfpy>=1.4.1 ; extra == 'excel' + - openpyxl>=3.1.0 ; extra == 'excel' + - python-calamine>=0.1.7 ; extra == 'excel' + - pyxlsb>=1.0.10 ; extra == 'excel' + - xlrd>=2.0.1 ; extra == 'excel' + - xlsxwriter>=3.0.5 ; extra == 'excel' + - pyarrow>=10.0.1 ; extra == 'parquet' + - pyarrow>=10.0.1 ; extra == 'feather' + - tables>=3.8.0 ; extra == 'hdf5' + - pyreadstat>=1.2.0 ; extra == 'spss' + - sqlalchemy>=2.0.0 ; extra == 'postgresql' + - psycopg2>=2.9.6 ; extra == 'postgresql' + - adbc-driver-postgresql>=0.8.0 ; extra == 'postgresql' + - sqlalchemy>=2.0.0 ; extra == 'mysql' + - pymysql>=1.0.2 ; extra == 'mysql' + - sqlalchemy>=2.0.0 ; extra == 'sql-other' + - adbc-driver-postgresql>=0.8.0 ; extra == 'sql-other' + - adbc-driver-sqlite>=0.8.0 ; extra == 'sql-other' + - beautifulsoup4>=4.11.2 ; extra == 'html' + - html5lib>=1.1 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'html' + - lxml>=4.9.2 ; extra == 'xml' + - matplotlib>=3.6.3 ; extra == 'plot' + - jinja2>=3.1.2 ; extra == 'output-formatting' + - tabulate>=0.9.0 ; extra == 'output-formatting' + - pyqt5>=5.15.9 ; extra == 'clipboard' + - qtpy>=2.3.0 ; extra == 'clipboard' + - zstandard>=0.19.0 ; extra == 'compression' + - dataframe-api-compat>=0.1.7 ; extra == 'consortium-standard' + - adbc-driver-postgresql>=0.8.0 ; extra == 'all' + - adbc-driver-sqlite>=0.8.0 ; extra == 'all' + - beautifulsoup4>=4.11.2 ; extra == 'all' + - bottleneck>=1.3.6 ; extra == 'all' + - dataframe-api-compat>=0.1.7 ; extra == 'all' + - fastparquet>=2022.12.0 ; extra == 'all' + - fsspec>=2022.11.0 ; extra == 'all' + - gcsfs>=2022.11.0 ; extra == 'all' + - html5lib>=1.1 ; extra == 'all' + - hypothesis>=6.46.1 ; extra == 'all' + - jinja2>=3.1.2 ; extra == 'all' + - lxml>=4.9.2 ; extra == 'all' + - matplotlib>=3.6.3 ; extra == 'all' + - numba>=0.56.4 ; extra == 'all' + - numexpr>=2.8.4 ; extra == 'all' + - odfpy>=1.4.1 ; extra == 'all' + - openpyxl>=3.1.0 ; extra == 'all' + - pandas-gbq>=0.19.0 ; extra == 'all' + - psycopg2>=2.9.6 ; extra == 'all' + - pyarrow>=10.0.1 ; extra == 'all' + - pymysql>=1.0.2 ; extra == 'all' + - pyqt5>=5.15.9 ; extra == 'all' + - pyreadstat>=1.2.0 ; extra == 'all' + - pytest>=7.3.2 ; extra == 'all' + - pytest-xdist>=2.2.0 ; extra == 'all' + - python-calamine>=0.1.7 ; extra == 'all' + - pyxlsb>=1.0.10 ; extra == 'all' + - qtpy>=2.3.0 ; extra == 'all' + - scipy>=1.10.0 ; extra == 'all' + - s3fs>=2022.11.0 ; extra == 'all' + - sqlalchemy>=2.0.0 ; extra == 'all' + - tables>=3.8.0 ; extra == 'all' + - tabulate>=0.9.0 ; extra == 'all' + - xarray>=2022.12.0 ; extra == 'all' + - xlrd>=2.0.1 ; extra == 'all' + - xlsxwriter>=3.0.5 ; extra == 'all' + - zstandard>=0.19.0 ; extra == 'all' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl name: pandas - version: 2.3.2 - sha256: 4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b + version: 2.3.3 + sha256: bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8 requires_dist: - numpy>=1.22.4 ; python_full_version < '3.11' - numpy>=1.23.2 ; python_full_version == '3.11.*' @@ -1972,6 +4737,46 @@ packages: purls: [] size: 455420 timestamp: 1751292466873 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pango-1.56.4-he55ef5b_0.conda + sha256: dd36cd5b6bc1c2988291a6db9fa4eb8acade9b487f6f1da4eaa65a1eebb0a12d + md5: a22cc88bf6059c9bcc158c94c9aab5b8 + depends: + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=13 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 468811 + timestamp: 1751293869070 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + sha256: 705484ad60adee86cab1aad3d2d8def03a699ece438c864e8ac995f6f66401a6 + md5: 7d57f8b4b7acfc75c777bc231f0d31be + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 426931 + timestamp: 1751292636271 - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl name: parso version: 0.8.5 @@ -2001,6 +4806,30 @@ packages: purls: [] size: 1209177 timestamp: 1756742976157 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pcre2-10.46-h15761aa_0.conda + sha256: 75800e60e0e44d957c691a964085f56c9ac37dcd75e6c6904809d7b68f39e4ea + md5: 5128cb5188b630a58387799ea1366e37 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 1161914 + timestamp: 1756742893031 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.46-h7125dd6_0.conda + sha256: 5bf2eeaa57aab6e8e95bea6bd6bb2a739f52eb10572d8ed259d25864d3528240 + md5: 0e6e82c3cc3835f4692022e9b9cd5df8 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 835080 + timestamp: 1756743041908 - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl name: pexpect version: 4.9.0 @@ -2037,6 +4866,70 @@ packages: - typing-extensions ; python_full_version < '3.10' and extra == 'typing' - defusedxml ; extra == 'xmp' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + name: pillow + version: 12.0.0 + sha256: 0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl + name: pillow + version: 12.0.0 + sha256: 5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.4-h54a6638_1.conda sha256: 43d37bc9ca3b257c5dd7bf76a8426addbdec381f6786ff441dc90b1a49143b6a md5: c01af13bdc553d1a8fbfff6e8db075f0 @@ -2050,6 +4943,29 @@ packages: purls: [] size: 450960 timestamp: 1754665235234 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pixman-0.46.4-h7ac5ae9_1.conda + sha256: e6b0846a998f2263629cfeac7bca73565c35af13251969f45d385db537a514e4 + md5: 1587081d537bd4ae77d1c0635d465ba5 + depends: + - libgcc >=14 + - libstdcxx >=14 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 357913 + timestamp: 1754665583353 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda + sha256: 29c9b08a9b8b7810f9d4f159aecfd205fce051633169040005c0b7efad4bc718 + md5: 17c3d745db6ea72ae2fce17e7338547f + depends: + - __osx >=11.0 + - libcxx >=19 + license: MIT + license_family: MIT + purls: [] + size: 248045 + timestamp: 1754665282033 - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl name: platformdirs version: 4.4.0 @@ -2066,6 +4982,22 @@ packages: - pytest>=8.3.4 ; extra == 'test' - mypy>=1.14.1 ; extra == 'type' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl + name: platformdirs + version: 4.5.0 + sha256: e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3 + requires_dist: + - furo>=2025.9.25 ; extra == 'docs' + - proselint>=0.14 ; extra == 'docs' + - sphinx-autodoc-typehints>=3.2 ; extra == 'docs' + - sphinx>=8.2.3 ; extra == 'docs' + - appdirs==1.4.4 ; extra == 'test' + - covdefaults>=2.3 ; extra == 'test' + - pytest-cov>=7 ; extra == 'test' + - pytest-mock>=3.15.1 ; extra == 'test' + - pytest>=8.4.2 ; extra == 'test' + - mypy>=1.18.2 ; extra == 'type' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl name: pluggy version: 1.6.0 @@ -2079,15 +5011,26 @@ packages: requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl name: pre-commit - version: 4.3.0 - sha256: 2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 + version: 4.3.0 + sha256: 2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 + requires_dist: + - cfgv>=2.0.0 + - identify>=1.0.0 + - nodeenv>=0.11.1 + - pyyaml>=5.1 + - virtualenv>=20.10.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl + name: pre-commit + version: 4.4.0 + sha256: b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813 requires_dist: - cfgv>=2.0.0 - identify>=1.0.0 - nodeenv>=0.11.1 - pyyaml>=5.1 - virtualenv>=20.10.0 - requires_python: '>=3.9' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl name: prompt-toolkit version: 3.0.52 @@ -2106,6 +5049,16 @@ packages: purls: [] size: 8252 timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-h86ecc28_1002.conda + sha256: 977dfb0cb3935d748521dd80262fe7169ab82920afd38ed14b7fee2ea5ec01ba + md5: bb5a90c93e3bac3d5690acf76b4a6386 + depends: + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 8342 + timestamp: 1726803319942 - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl name: ptyprocess version: 0.7.0 @@ -2126,11 +5079,21 @@ packages: version: '2.23' sha256: e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: pycryptodome + version: 3.23.0 + sha256: 67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: pycryptodome version: 3.23.0 sha256: c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + name: pycryptodome + version: 3.23.0 + sha256: 187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl name: pydot version: 4.0.1 @@ -2178,6 +5141,14 @@ packages: - railroad-diagrams ; extra == 'diagrams' - jinja2 ; extra == 'diagrams' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl + name: pyparsing + version: 3.2.5 + sha256: e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e + requires_dist: + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl name: pytest version: 8.4.2 @@ -2198,6 +5169,26 @@ packages: - setuptools ; extra == 'dev' - xmlschema ; extra == 'dev' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl + name: pytest + version: 9.0.0 + sha256: e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96 + requires_dist: + - colorama>=0.4 ; sys_platform == 'win32' + - exceptiongroup>=1 ; python_full_version < '3.11' + - iniconfig>=1.0.1 + - packaging>=22 + - pluggy>=1.5,<2 + - pygments>=2.7.2 + - tomli>=1 ; python_full_version < '3.11' + - argcomplete ; extra == 'dev' + - attrs>=19.2 ; extra == 'dev' + - hypothesis>=3.56 ; extra == 'dev' + - mock ; extra == 'dev' + - requests ; extra == 'dev' + - setuptools ; extra == 'dev' + - xmlschema ; extra == 'dev' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl name: pytest-cov version: 7.0.0 @@ -2221,6 +5212,17 @@ packages: - coverage>=7.6.1 ; extra == 'testing' - pytest-mock>=3.14 ; extra == 'testing' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl + name: pytest-env + version: 1.2.0 + sha256: d7e5b7198f9b83c795377c09feefa45d56083834e60d04767efd64819fc9da00 + requires_dist: + - pytest>=8.4.2 + - tomli>=2.2.1 ; python_full_version < '3.11' + - covdefaults>=2.3 ; extra == 'testing' + - coverage>=7.10.7 ; extra == 'testing' + - pytest-mock>=3.15.1 ; extra == 'testing' + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.7-h2b335a9_100_cp313.conda build_number: 100 sha256: 16cc30a5854f31ca6c3688337d34e37a79cdc518a06375fe3482ea8e2d6b34c8 @@ -2248,6 +5250,56 @@ packages: size: 33583088 timestamp: 1756911465277 python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.9-h4c0d347_101_cp313.conda + build_number: 101 + sha256: 95f11d8f8e8007ead0927ff15401a9a48a28df92b284f41a08824955c009e974 + md5: b62a2e7c210e4bffa9aaa041f7152a25 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libuuid >=2.41.2,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33737136 + timestamp: 1761175607146 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.9-hfc2f54d_101_cp313.conda + build_number: 101 + sha256: 516229f780b98783a5ef4112a5a4b5e5647d4f0177c4621e98aa60bb9bc32f98 + md5: a4241bce59eecc74d4d2396e108c93b8 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.1,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.50.4,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.4,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 11915380 + timestamp: 1761176793936 + python_site_packages_path: lib/python3.13/site-packages - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl name: python-dateutil version: 2.9.0.post0 @@ -2275,6 +5327,16 @@ packages: version: 6.0.2 sha256: 70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: pyyaml + version: 6.0.3 + sha256: ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + name: pyyaml + version: 6.0.3 + sha256: 2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c md5: 283b96675859b20a825f8fa30f311446 @@ -2286,6 +5348,27 @@ packages: purls: [] size: 282480 timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8382b9d_2.conda + sha256: 54bed3a3041befaa9f5acde4a37b1a02f44705b7796689574bcf9d7beaad2959 + md5: c0f08fc2737967edde1a272d4bf41ed9 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 291806 + timestamp: 1740380591358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 + md5: 63ef3f6e6d6d5c589e64f11263dc5676 + depends: + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 252359 + timestamp: 1740379663071 - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl name: requests version: 2.32.5 @@ -2385,6 +5468,28 @@ packages: purls: [] size: 3285204 timestamp: 1748387766691 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5688188_102.conda + sha256: 46e10488e9254092c655257c18fcec0a9864043bdfbe935a9fbf4fb2028b8514 + md5: 2562c9bfd1de3f9c590f0fe53858d85c + depends: + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3342845 + timestamp: 1748393219221 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + sha256: cb86c522576fa95c6db4c878849af0bccfd3264daf0cc40dd18e7f4a7bfced0e + md5: 7362396c170252e7b7b0c8fb37fe9c78 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3125538 + timestamp: 1748388189063 - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl name: tqdm version: 4.67.1 @@ -2474,6 +5579,36 @@ packages: - setuptools>=68 ; extra == 'test' - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl + name: virtualenv + version: 20.35.4 + sha256: c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b + requires_dist: + - distlib>=0.3.7,<1 + - filelock>=3.12.2,<4 + - importlib-metadata>=6.6 ; python_full_version < '3.8' + - platformdirs>=3.9.1,<5 + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - furo>=2023.7.26 ; extra == 'docs' + - proselint>=0.13 ; extra == 'docs' + - sphinx>=7.1.2,!=7.3 ; extra == 'docs' + - sphinx-argparse>=0.4 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.2.1a0 ; extra == 'docs' + - towncrier>=23.6 ; extra == 'docs' + - covdefaults>=2.3 ; extra == 'test' + - coverage-enable-subprocess>=1 ; extra == 'test' + - coverage>=7.2.7 ; extra == 'test' + - flaky>=3.7 ; extra == 'test' + - packaging>=23.1 ; extra == 'test' + - pytest-env>=0.8.2 ; extra == 'test' + - pytest-freezer>=0.4.8 ; (python_full_version >= '3.13' and platform_python_implementation == 'CPython' and sys_platform == 'win32' and extra == 'test') or (platform_python_implementation == 'GraalVM' and extra == 'test') or (platform_python_implementation == 'PyPy' and extra == 'test') + - pytest-mock>=3.11.1 ; extra == 'test' + - pytest-randomly>=3.12 ; extra == 'test' + - pytest-timeout>=2.1 ; extra == 'test' + - pytest>=7.4 ; extra == 'test' + - setuptools>=68 ; extra == 'test' + - time-machine>=2.10 ; platform_python_implementation == 'CPython' and extra == 'test' + requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda sha256: ba673427dcd480cfa9bbc262fd04a9b1ad2ed59a159bd8f7e750d4c52282f34c md5: 0f2ca7906bf166247d1d760c3422cb8a @@ -2488,12 +5623,30 @@ packages: purls: [] size: 330474 timestamp: 1751817998141 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/wayland-1.24.0-h4f8a99f_1.conda + sha256: d94af8f287db764327ac7b48f6c0cd5c40da6ea2606afd34ac30671b7c85d8ee + md5: f6966cb1f000c230359ae98c29e37d87 + depends: + - libexpat >=2.7.1,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 331480 + timestamp: 1761174368396 - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl name: wcwidth version: 0.2.13 sha256: 3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859 requires_dist: - backports-functools-lru-cache>=1.2.1 ; python_full_version < '3.2' +- pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl + name: wcwidth + version: 0.2.14 + sha256: a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 + requires_python: '>=3.6' - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda sha256: a5d4af601f71805ec67403406e147c48d6bad7aaeae92b0622b7e2396842d3fe md5: 397a013c2dc5145a70737871aaa87e98 @@ -2506,6 +5659,17 @@ packages: purls: [] size: 392406 timestamp: 1749375847832 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xkeyboard-config-2.46-he30d5cf_0.conda + sha256: c440a757d210e84c7f315ac3b034266980a8b4c986600649d296b9198b5b4f5e + md5: 9524f30d9dea7dd5d6ead43a8823b6c2 + depends: + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 396706 + timestamp: 1759543850920 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b md5: fb901ff28063514abb6046c9ec2c4a45 @@ -2517,6 +5681,16 @@ packages: purls: [] size: 58628 timestamp: 1734227592886 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libice-1.1.2-h86ecc28_0.conda + sha256: a2ba1864403c7eb4194dacbfe2777acf3d596feae43aada8d1b478617ce45031 + md5: c8d8ec3e00cd0fd8a231789b91a7c5b7 + depends: + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 60433 + timestamp: 1734229908988 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 md5: 1c74ff8c35dcadf952a16f752ca5aa49 @@ -2530,6 +5704,18 @@ packages: purls: [] size: 27590 timestamp: 1741896361728 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libsm-1.2.6-h0808dbd_0.conda + sha256: b86a819cd16f90c01d9d81892155126d01555a20dabd5f3091da59d6309afd0a + md5: 2d1409c50882819cb1af2de82e2b7208 + depends: + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - xorg-libice >=1.1.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 28701 + timestamp: 1741897678254 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda sha256: 51909270b1a6c5474ed3978628b341b4d4472cd22610e5f22b506855a5e20f67 md5: db038ce880f100acc74dba10302b5630 @@ -2542,6 +5728,17 @@ packages: purls: [] size: 835896 timestamp: 1741901112627 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libx11-1.8.12-hca56bd8_0.conda + sha256: 452977d8ad96f04ec668ba74f46e70a53e00f99c0e0307956aeca75894c8131d + md5: 3df132f0048b9639bc091ef22937c111 + depends: + - libgcc >=13 + - libxcb >=1.17.0,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 864850 + timestamp: 1741901264068 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda sha256: ed10c9283974d311855ae08a16dfd7e56241fac632aec3b92e3cfe73cff31038 md5: f6ebe2cb3f82ba6c057dde5d9debe4f7 @@ -2553,6 +5750,16 @@ packages: purls: [] size: 14780 timestamp: 1734229004433 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.12-h86ecc28_0.conda + sha256: 7829a0019b99ba462aece7592d2d7f42e12d12ccd3b9614e529de6ddba453685 + md5: d5397424399a66d33c80b1f2345a36a6 + depends: + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 15873 + timestamp: 1734230458294 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda sha256: 753f73e990c33366a91fd42cc17a3d19bb9444b9ca5ff983605fa9e953baf57f md5: d3c295b50f092ab525ffe3c2aa4b7413 @@ -2566,6 +5773,18 @@ packages: purls: [] size: 13603 timestamp: 1727884600744 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcomposite-0.4.6-h86ecc28_2.conda + sha256: 0cb82160412adb6d83f03cf50e807a8e944682d556b2215992a6fbe9ced18bc0 + md5: 86051eee0766c3542be24844a9c3cf36 + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13982 + timestamp: 1727884626338 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a md5: 2ccd714aa2242315acaf0a67faea780b @@ -2580,6 +5799,19 @@ packages: purls: [] size: 32533 timestamp: 1730908305254 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxcursor-1.2.3-h86ecc28_0.conda + sha256: c5d3692520762322a9598e7448492309f5ee9d8f3aff72d787cf06e77c42507f + md5: f2054759c2203d12d0007005e1f1296d + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + purls: [] + size: 34596 + timestamp: 1730908388714 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda sha256: 43b9772fd6582bf401846642c4635c47a9b0e36ca08116b3ec3df36ab96e0ec0 md5: b5fcc7172d22516e1f965490e65e33a4 @@ -2594,6 +5826,19 @@ packages: purls: [] size: 13217 timestamp: 1727891438799 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdamage-1.1.6-h86ecc28_0.conda + sha256: 3afaa2f43eb4cb679fc0c3d9d7c50f0f2c80cc5d3df01d5d5fd60655d0bfa9be + md5: d5773c4e4d64428d7ddaa01f6f845dc7 + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13794 + timestamp: 1727891406431 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda sha256: 6b250f3e59db07c2514057944a3ea2044d6a8cdde8a47b6497c254520fade1ee md5: 8035c64cb77ed555e3f150b7b3972480 @@ -2605,6 +5850,16 @@ packages: purls: [] size: 19901 timestamp: 1727794976192 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.5-h57736b2_0.conda + sha256: efcc150da5926cf244f757b8376d96a4db78bc15b8d90ca9f56ac6e75755971f + md5: 25a5a7b797fe6e084e04ffe2db02fc62 + depends: + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 20615 + timestamp: 1727796660574 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda sha256: da5dc921c017c05f38a38bd75245017463104457b63a1ce633ed41f214159c14 md5: febbab7d15033c913d53c7a2c102309d @@ -2617,6 +5872,17 @@ packages: purls: [] size: 50060 timestamp: 1727752228921 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxext-1.3.6-h57736b2_0.conda + sha256: 8e216b024f52e367463b4173f237af97cf7053c77d9ce3e958bc62473a053f71 + md5: bd1e86dd8aa3afd78a4bfdb4ef918165 + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 50746 + timestamp: 1727754268156 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda sha256: 2fef37e660985794617716eb915865ce157004a4d567ed35ec16514960ae9271 md5: 4bdb303603e9821baf5fe5fdff1dc8f8 @@ -2629,6 +5895,17 @@ packages: purls: [] size: 19575 timestamp: 1727794961233 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxfixes-6.0.2-he30d5cf_0.conda + sha256: 8cb9c88e25c57e47419e98f04f9ef3154ad96b9f858c88c570c7b91216a64d0e + md5: e8b4056544341daf1d415eaeae7a040c + depends: + - libgcc >=14 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 20704 + timestamp: 1759284028146 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda sha256: 1a724b47d98d7880f26da40e45f01728e7638e6ec69f35a3e11f92acd05f9e7a md5: 17dcc85db3c7886650b8908b183d6876 @@ -2643,6 +5920,19 @@ packages: purls: [] size: 47179 timestamp: 1727799254088 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxi-1.8.2-h57736b2_0.conda + sha256: 7b587407ecb9ccd2bbaf0fb94c5dbdde4d015346df063e9502dc0ce2b682fb5e + md5: eeee3bdb31c6acde2b81ad1b8c287087 + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 48197 + timestamp: 1727801059062 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda sha256: 1b9141c027f9d84a9ee5eb642a0c19457c788182a5a73c5a9083860ac5c20a8c md5: 5e2eb9bf77394fc2e5918beefec9f9ab @@ -2657,6 +5947,19 @@ packages: purls: [] size: 13891 timestamp: 1727908521531 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxinerama-1.1.5-h5ad3122_1.conda + sha256: 5f84f820397db504e187754665d48d385e0a2a49f07ffc2372c7f42fa36dd972 + md5: a7b99f104e14b99ca773d2fe2d195585 + depends: + - libgcc >=13 + - libstdcxx >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 14388 + timestamp: 1727908606602 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda sha256: ac0f037e0791a620a69980914a77cb6bb40308e26db11698029d6708f5aa8e0d md5: 2de7f99d6581a4a7adbff607b5c278ca @@ -2671,6 +5974,19 @@ packages: purls: [] size: 29599 timestamp: 1727794874300 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrandr-1.5.4-h86ecc28_0.conda + sha256: b2588a2b101d1b0a4e852532c8b9c92c59ef584fc762dd700567bdbf8cd00650 + md5: dd3e74283a082381aa3860312e3c721e + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + purls: [] + size: 30197 + timestamp: 1727794957221 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 md5: 96d57aba173e878a2089d5638016dc5e @@ -2683,6 +5999,17 @@ packages: purls: [] size: 33005 timestamp: 1734229037766 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxrender-0.9.12-h86ecc28_0.conda + sha256: ffd77ee860c9635a28cfda46163dcfe9224dc6248c62404c544ae6b564a0be1f + md5: ae2c2dd0e2d38d249887727db2af960e + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 33649 + timestamp: 1734229123157 - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda sha256: 752fdaac5d58ed863bbf685bb6f98092fe1a488ea8ebb7ed7b606ccfce08637a md5: 7bbe9a0cc0df0ac5f5a8ad6d6a11af2f @@ -2697,6 +6024,41 @@ packages: purls: [] size: 32808 timestamp: 1727964811275 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxtst-1.2.5-h57736b2_3.conda + sha256: 6eaffce5a34fc0a16a21ddeaefb597e792a263b1b0c387c1ce46b0a967d558e1 + md5: c05698071b5c8e0da82a282085845860 + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.9,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxi >=1.7.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 33786 + timestamp: 1727964907993 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda + sha256: 012f0d1fd9fb1d949e0dccc0b28d9dd5a8895a1f3e2a7edc1fa2e1b33fc0f233 + md5: d745faa2d7c15092652e40a22bb261ed + depends: + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 18185 + timestamp: 1734214652726 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda + sha256: 3dbbf4cdb5ad82d3479ab2aa68ae67de486a6d57d67f0402d8e55869f6f13aec + md5: 91cef7867bf2b47f614597b59705ff56 + depends: + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 566948 + timestamp: 1726847598167 - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 @@ -2710,3 +6072,26 @@ packages: purls: [] size: 567578 timestamp: 1742433379869 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + sha256: 0812e7b45f087cfdd288690ada718ce5e13e8263312e03b643dd7aa50d08b51b + md5: 5be90c5a3e4b43c53e38f50a85e11527 + depends: + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 551176 + timestamp: 1742433378347 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + sha256: 0d02046f57f7a1a3feae3e9d1aa2113788311f3cf37a3244c71e61a93177ba67 + md5: e6f69c7bcccdefa417f056fa593b40f0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 399979 + timestamp: 1742433432699 diff --git a/pyproject.toml b/pyproject.toml index a8deffa40..ba02c5e7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ "urllib3", "setuptools", ] -requires-python = ">=3.9,<4.0" +requires-python = ">=3.9,<3.14" authors = [ {name = "Dimitri Yatsenko", email = "dimitri@datajoint.com"}, {name = "Thinh Nguyen", email = "thinh@datajoint.com"}, @@ -125,7 +125,7 @@ JUPYTER_PASSWORD="datajoint" [tool.pixi.workspace] channels = ["conda-forge"] -platforms = ["linux-64"] +platforms = ["linux-64", "osx-arm64", "linux-aarch64"] [tool.pixi.pypi-dependencies] datajoint = { path = ".", editable = true } @@ -138,4 +138,8 @@ test = { features = ["test"], solve-group = "default" } [tool.pixi.tasks] [tool.pixi.dependencies] +python = ">=3.9,<3.14" graphviz = ">=13.1.2,<14" + +[tool.pixi.activation] +scripts=["activate.sh"] \ No newline at end of file From 88ca4dc3e4d158d835d485fce20c27bd72cd8926 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 10 Nov 2025 07:31:11 +0100 Subject: [PATCH 021/219] refactor test fixtures --- .pre-commit-config.yaml | 2 +- tests/conftest.py | 116 +++++++++++++++++++++++++++++++-- tests/test_alter.py | 12 ++-- tests/test_autopopulate.py | 14 ++-- tests/test_cascading_delete.py | 11 ++++ tests/test_declare.py | 10 +-- tests/test_jobs.py | 14 ++-- tests/test_relation.py | 18 ++--- tests/test_schema.py | 9 +-- 9 files changed, 162 insertions(+), 44 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccf72ed80..c112580d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: rev: 25.1.0 # matching versions in pyproject.toml and github actions hooks: - id: black - args: ["--check", "-v", "src", "tests", "--diff"] # --required-version is conflicting with pre-commit + args: ["-v", "src", "tests", "--diff"] # --required-version is conflicting with pre-commit - repo: https://github.com/PyCQA/flake8 rev: 7.3.0 hooks: diff --git a/tests/conftest.py b/tests/conftest.py index 2c16f1140..beebd09e0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,6 +45,57 @@ def pytest_configure(config): pass +@pytest.fixture +def clean_autopopulate(experiment, trial, ephys): + """ + Explicit cleanup fixture for autopopulate tests. + + Cleans experiment/trial/ephys tables after test completes. + Tests must explicitly request this fixture to get cleanup. + """ + yield + # Cleanup after test - delete in reverse dependency order + ephys.delete() + trial.delete() + experiment.delete() + + +@pytest.fixture +def clean_jobs(schema_any): + """ + Explicit cleanup fixture for jobs tests. + + Cleans jobs table before test runs. + Tests must explicitly request this fixture to get cleanup. + """ + try: + schema_any.jobs.delete() + except DataJointError: + pass + yield + + +@pytest.fixture +def clean_test_tables(test, test_extra, test_no_extra): + """ + Explicit cleanup fixture for relation tests using test tables. + + Ensures test table has lookup data and restores clean state after test. + Tests must explicitly request this fixture to get cleanup. + """ + # Ensure lookup data exists before test + if not test: + test.insert(test.contents, skip_duplicates=True) + + yield + + # Restore original state after test + test.delete() + test.insert(test.contents, skip_duplicates=True) + test_extra.delete() + test_no_extra.delete() + + # Global container registry for cleanup _active_containers = set() _docker_client = None @@ -547,7 +598,7 @@ def mock_cache(tmpdir_factory): dj.config["cache"] = og_cache -@pytest.fixture +@pytest.fixture(scope="module") def schema_any(connection_test, prefix): schema_any = dj.Schema( prefix + "_test1", schema.LOCALS_ANY, connection=connection_test @@ -603,6 +654,63 @@ def schema_any(connection_test, prefix): schema_any.drop() +@pytest.fixture +def schema_any_fresh(connection_test, prefix): + """Function-scoped schema_any for tests that need fresh schema state.""" + schema_any = dj.Schema( + prefix + "_test1_fresh", schema.LOCALS_ANY, connection=connection_test + ) + assert schema.LOCALS_ANY, "LOCALS_ANY is empty" + try: + schema_any.jobs.delete() + except DataJointError: + pass + schema_any(schema.TTest) + schema_any(schema.TTest2) + schema_any(schema.TTest3) + schema_any(schema.NullableNumbers) + schema_any(schema.TTestExtra) + schema_any(schema.TTestNoExtra) + schema_any(schema.Auto) + schema_any(schema.User) + schema_any(schema.Subject) + schema_any(schema.Language) + schema_any(schema.Experiment) + schema_any(schema.Trial) + schema_any(schema.Ephys) + schema_any(schema.Image) + schema_any(schema.UberTrash) + schema_any(schema.UnterTrash) + schema_any(schema.SimpleSource) + schema_any(schema.SigIntTable) + schema_any(schema.SigTermTable) + schema_any(schema.DjExceptionName) + schema_any(schema.ErrorClass) + schema_any(schema.DecimalPrimaryKey) + schema_any(schema.IndexRich) + schema_any(schema.ThingA) + schema_any(schema.ThingB) + schema_any(schema.ThingC) + schema_any(schema.ThingD) + schema_any(schema.ThingE) + schema_any(schema.Parent) + schema_any(schema.Child) + schema_any(schema.ComplexParent) + schema_any(schema.ComplexChild) + schema_any(schema.SubjectA) + schema_any(schema.SessionA) + schema_any(schema.SessionStatusA) + schema_any(schema.SessionDateA) + schema_any(schema.Stimulus) + schema_any(schema.Longblob) + yield schema_any + try: + schema_any.jobs.delete() + except DataJointError: + pass + schema_any.drop() + + @pytest.fixture def thing_tables(schema_any): a = schema.ThingA() @@ -623,7 +731,7 @@ def thing_tables(schema_any): yield a, b, c, d, e -@pytest.fixture +@pytest.fixture(scope="module") def schema_simp(connection_test, prefix): schema = dj.Schema( prefix + "_relational", schema_simple.LOCALS_SIMPLE, connection=connection_test @@ -653,7 +761,7 @@ def schema_simp(connection_test, prefix): schema.drop() -@pytest.fixture +@pytest.fixture(scope="module") def schema_adv(connection_test, prefix): schema = dj.Schema( prefix + "_advanced", @@ -694,7 +802,7 @@ def schema_ext( schema.drop() -@pytest.fixture +@pytest.fixture(scope="module") def schema_uuid(connection_test, prefix): schema = dj.Schema( prefix + "_test1", diff --git a/tests/test_alter.py b/tests/test_alter.py index 375d31d55..2013d313d 100644 --- a/tests/test_alter.py +++ b/tests/test_alter.py @@ -14,12 +14,12 @@ @pytest.fixture -def schema_alter(connection_test, schema_any): - # Overwrite Experiment and Parent nodes - schema_any(Experiment, context=LOCALS_ALTER) - schema_any(Parent, context=LOCALS_ALTER) - yield schema_any - schema_any.drop() +def schema_alter(connection_test, schema_any_fresh): + # Overwrite Experiment and Parent nodes using fresh schema + schema_any_fresh(Experiment, context=LOCALS_ALTER) + schema_any_fresh(Parent, context=LOCALS_ALTER) + yield schema_any_fresh + schema_any_fresh.drop() class TestAlter: diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py index 899d90d9e..268c7a973 100644 --- a/tests/test_autopopulate.py +++ b/tests/test_autopopulate.py @@ -7,7 +7,7 @@ from . import schema -def test_populate(trial, subject, experiment, ephys, channel): +def test_populate(clean_autopopulate, trial, subject, experiment, ephys, channel): # test simple populate assert subject, "root tables are empty" assert not experiment, "table already filled?" @@ -33,7 +33,7 @@ def test_populate(trial, subject, experiment, ephys, channel): assert channel -def test_populate_with_success_count(subject, experiment, trial): +def test_populate_with_success_count(clean_autopopulate, subject, experiment, trial): # test simple populate assert subject, "root tables are empty" assert not experiment, "table already filled?" @@ -51,7 +51,7 @@ def test_populate_with_success_count(subject, experiment, trial): assert len(trial.key_source & trial) == success_count -def test_populate_key_list(subject, experiment, trial): +def test_populate_key_list(clean_autopopulate, subject, experiment, trial): # test simple populate assert subject, "root tables are empty" assert not experiment, "table already filled?" @@ -63,7 +63,7 @@ def test_populate_key_list(subject, experiment, trial): assert n == ret["success_count"] -def test_populate_exclude_error_and_ignore_jobs(schema_any, subject, experiment): +def test_populate_exclude_error_and_ignore_jobs(clean_autopopulate, schema_any, subject, experiment): # test simple populate assert subject, "root tables are empty" assert not experiment, "table already filled?" @@ -79,7 +79,7 @@ def test_populate_exclude_error_and_ignore_jobs(schema_any, subject, experiment) assert len(experiment.key_source & experiment) == len(experiment.key_source) - 2 -def test_allow_direct_insert(subject, experiment): +def test_allow_direct_insert(clean_autopopulate, subject, experiment): assert subject, "root tables are empty" key = subject.fetch("KEY", limit=1)[0] key["experiment_id"] = 1000 @@ -88,14 +88,14 @@ def test_allow_direct_insert(subject, experiment): @pytest.mark.parametrize("processes", [None, 2]) -def test_multi_processing(subject, experiment, processes): +def test_multi_processing(clean_autopopulate, subject, experiment, processes): assert subject, "root tables are empty" assert not experiment, "table already filled?" experiment.populate(processes=None) assert len(experiment) == len(subject) * experiment.fake_experiments_per_subject -def test_allow_insert(subject, experiment): +def test_allow_insert(clean_autopopulate, subject, experiment): assert subject, "root tables are empty" key = subject.fetch("KEY")[0] key["experiment_id"] = 1001 diff --git a/tests/test_cascading_delete.py b/tests/test_cascading_delete.py index 71216fcb2..84cf56dbc 100644 --- a/tests/test_cascading_delete.py +++ b/tests/test_cascading_delete.py @@ -8,6 +8,17 @@ @pytest.fixture def schema_simp_pop(schema_simp): + # Clean up tables first to ensure fresh state with module-scoped schema + # Delete in reverse dependency order + Profile().delete() + Website().delete() + G().delete() + E().delete() + D().delete() + B().delete() + L().delete() + A().delete() + A().insert(A.contents, skip_duplicates=True) L().insert(L.contents, skip_duplicates=True) B().populate() diff --git a/tests/test_declare.py b/tests/test_declare.py index 828021939..6d616962e 100644 --- a/tests/test_declare.py +++ b/tests/test_declare.py @@ -268,7 +268,7 @@ class BadName(dj.Manual): schema_any(BadName) -def test_bad_fk_rename(schema_any): +def test_bad_fk_rename(schema_any_fresh): """issue #381""" class A(dj.Manual): @@ -281,9 +281,9 @@ class B(dj.Manual): b -> A # invalid, the new syntax is (b) -> A """ - schema_any(A) + schema_any_fresh(A) with pytest.raises(dj.DataJointError): - schema_any(B) + schema_any_fresh(B) def test_primary_nullable_foreign_key(schema_any): @@ -401,7 +401,7 @@ def test_add_hidden_timestamp_default_value(): ), "Default value for add_hidden_timestamp is not False" -def test_add_hidden_timestamp_enabled(enable_add_hidden_timestamp, schema_any): +def test_add_hidden_timestamp_enabled(enable_add_hidden_timestamp, schema_any_fresh): assert config["add_hidden_timestamp"], "add_hidden_timestamp is not enabled" msg = f"{Experiment().heading._attributes=}" assert any( @@ -414,7 +414,7 @@ def test_add_hidden_timestamp_enabled(enable_add_hidden_timestamp, schema_any): assert not any(a.is_hidden for a in Experiment().heading.attributes.values()), msg -def test_add_hidden_timestamp_disabled(disable_add_hidden_timestamp, schema_any): +def test_add_hidden_timestamp_disabled(disable_add_hidden_timestamp, schema_any_fresh): assert not config[ "add_hidden_timestamp" ], "expected add_hidden_timestamp to be False" diff --git a/tests/test_jobs.py b/tests/test_jobs.py index dc363076d..a15283668 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -9,7 +9,7 @@ from . import schema -def test_reserve_job(subject, schema_any): +def test_reserve_job(clean_jobs, subject, schema_any): assert subject table_name = "fake_table" @@ -47,7 +47,7 @@ def test_reserve_job(subject, schema_any): assert not schema_any.jobs, "failed to clear error jobs" -def test_restrictions(schema_any): +def test_restrictions(clean_jobs, schema_any): jobs = schema_any.jobs jobs.delete() jobs.reserve("a", {"key": "a1"}) @@ -62,7 +62,7 @@ def test_restrictions(schema_any): jobs.delete() -def test_sigint(schema_any): +def test_sigint(clean_jobs, schema_any): try: schema.SigIntTable().populate(reserve_jobs=True) except KeyboardInterrupt: @@ -74,7 +74,7 @@ def test_sigint(schema_any): assert error_message == "KeyboardInterrupt" -def test_sigterm(schema_any): +def test_sigterm(clean_jobs, schema_any): try: schema.SigTermTable().populate(reserve_jobs=True) except SystemExit: @@ -86,14 +86,14 @@ def test_sigterm(schema_any): assert error_message == "SystemExit: SIGTERM received" -def test_suppress_dj_errors(schema_any): +def test_suppress_dj_errors(clean_jobs, schema_any): """test_suppress_dj_errors: dj errors suppressible w/o native py blobs""" with dj.config(enable_python_native_blobs=False): schema.ErrorClass.populate(reserve_jobs=True, suppress_errors=True) assert len(schema.DjExceptionName()) == len(schema_any.jobs) > 0 -def test_long_error_message(subject, schema_any): +def test_long_error_message(clean_jobs, subject, schema_any): # create long error message long_error_message = "".join( random.choice(string.ascii_letters) for _ in range(ERROR_MESSAGE_LENGTH + 100) @@ -129,7 +129,7 @@ def test_long_error_message(subject, schema_any): schema_any.jobs.delete() -def test_long_error_stack(subject, schema_any): +def test_long_error_stack(clean_jobs, subject, schema_any): # create long error stack STACK_SIZE = ( 89942 # Does not fit into small blob (should be 64k, but found to be higher) diff --git a/tests/test_relation.py b/tests/test_relation.py index 565e1eafa..d13abce1a 100644 --- a/tests/test_relation.py +++ b/tests/test_relation.py @@ -80,7 +80,7 @@ def test_wrong_insert_type(user): user.insert1(3) -def test_insert_select(subject, test, test2): +def test_insert_select(clean_test_tables, subject, test, test2): test2.delete() test2.insert(test) assert len(test2) == len(test) @@ -98,7 +98,7 @@ def test_insert_select(subject, test, test2): assert len(subject) == 2 * original_length -def test_insert_pandas_roundtrip(test, test2): +def test_insert_pandas_roundtrip(clean_test_tables, test, test2): """ensure fetched frames can be inserted""" test2.delete() n = len(test) @@ -110,7 +110,7 @@ def test_insert_pandas_roundtrip(test, test2): assert len(test2) == n -def test_insert_pandas_userframe(test, test2): +def test_insert_pandas_userframe(clean_test_tables, test, test2): """ ensure simple user-created frames (1 field, non-custom index) can be inserted without extra index adjustment @@ -125,14 +125,14 @@ def test_insert_pandas_userframe(test, test2): assert len(test2) == n -def test_insert_select_ignore_extra_fields0(test, test_extra): +def test_insert_select_ignore_extra_fields0(clean_test_tables, test, test_extra): """need ignore extra fields for insert select""" test_extra.insert1((test.fetch("key").max() + 1, 0, 0)) with pytest.raises(dj.DataJointError): test.insert(test_extra) -def test_insert_select_ignore_extra_fields1(test, test_extra): +def test_insert_select_ignore_extra_fields1(clean_test_tables, test, test_extra): """make sure extra fields works in insert select""" test_extra.delete() keyno = test.fetch("key").max() + 1 @@ -141,13 +141,15 @@ def test_insert_select_ignore_extra_fields1(test, test_extra): assert keyno in test.fetch("key") -def test_insert_select_ignore_extra_fields2(test_no_extra, test): +def test_insert_select_ignore_extra_fields2(clean_test_tables, test_no_extra, test): """make sure insert select still works when ignoring extra fields when there are none""" test_no_extra.delete() test_no_extra.insert(test, ignore_extra_fields=True) -def test_insert_select_ignore_extra_fields3(test, test_no_extra, test_extra): +def test_insert_select_ignore_extra_fields3( + clean_test_tables, test, test_no_extra, test_extra +): """make sure insert select works for from query result""" # Recreate table state from previous tests keyno = test.fetch("key").max() + 1 @@ -161,7 +163,7 @@ def test_insert_select_ignore_extra_fields3(test, test_no_extra, test_extra): test_no_extra.insert((test_extra & "`key`=" + keystr), ignore_extra_fields=True) -def test_skip_duplicates(test_no_extra, test): +def test_skip_duplicates(clean_test_tables, test_no_extra, test): """test that skip_duplicates works when inserting from another table""" test_no_extra.delete() test_no_extra.insert(test, ignore_extra_fields=True, skip_duplicates=True) diff --git a/tests/test_schema.py b/tests/test_schema.py index fb3cfa752..437ca93b7 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -7,11 +7,7 @@ import datajoint as dj from . import schema - - -class Ephys(dj.Imported): - definition = """ # This is already declare in ./schema.py - """ +from .schema import Ephys def relation_selector(attr): @@ -52,6 +48,7 @@ def schema_empty_module(schema_any, schema_empty): @pytest.fixture def schema_empty(connection_test, schema_any, prefix): context = {**schema.LOCALS_ANY, "Ephys": Ephys} + # Use the same database as schema_any so spawn_missing_classes can find the tables schema_empty = dj.Schema( prefix + "_test1", context=context, connection=connection_test ) @@ -59,7 +56,7 @@ def schema_empty(connection_test, schema_any, prefix): # load the rest of the classes schema_empty.spawn_missing_classes(context=context) yield schema_empty - schema_empty.drop() + # Don't drop the schema since schema_any still needs it def test_schema_size_on_disk(schema_any): From a30d41be1b4ea287dd20933546397b7cd8fbcd65 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 10 Nov 2025 07:31:46 +0100 Subject: [PATCH 022/219] skip multiprocessing tests on osx --- tests/test_autopopulate.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_autopopulate.py b/tests/test_autopopulate.py index 268c7a973..4bf0ed767 100644 --- a/tests/test_autopopulate.py +++ b/tests/test_autopopulate.py @@ -1,3 +1,5 @@ +import platform + import pymysql import pytest @@ -63,7 +65,9 @@ def test_populate_key_list(clean_autopopulate, subject, experiment, trial): assert n == ret["success_count"] -def test_populate_exclude_error_and_ignore_jobs(clean_autopopulate, schema_any, subject, experiment): +def test_populate_exclude_error_and_ignore_jobs( + clean_autopopulate, schema_any, subject, experiment +): # test simple populate assert subject, "root tables are empty" assert not experiment, "table already filled?" @@ -87,11 +91,15 @@ def test_allow_direct_insert(clean_autopopulate, subject, experiment): experiment.insert1(key, allow_direct_insert=True) +@pytest.mark.skipif( + platform.system() == "Darwin", + reason="multiprocessing with spawn method (macOS default) cannot pickle thread locks", +) @pytest.mark.parametrize("processes", [None, 2]) def test_multi_processing(clean_autopopulate, subject, experiment, processes): assert subject, "root tables are empty" assert not experiment, "table already filled?" - experiment.populate(processes=None) + experiment.populate(processes=processes) assert len(experiment) == len(subject) * experiment.fake_experiments_per_subject From d631b8b62954df72084c007869f545037f202ff7 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 10 Nov 2025 09:07:39 +0100 Subject: [PATCH 023/219] skip c901 check --- src/datajoint/diagram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datajoint/diagram.py b/src/datajoint/diagram.py index aa505fb54..77da7d637 100644 --- a/src/datajoint/diagram.py +++ b/src/datajoint/diagram.py @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__.split(".")[0]) -if not diagram_active: +if not diagram_active: # noqa: C901 class Diagram: """ From f45e7c8b788fefe8f8100c76af3b329aba8c2626 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 10 Nov 2025 09:14:19 +0100 Subject: [PATCH 024/219] update pre-commit --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c112580d1..4461fb878 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,6 @@ repos: - --max-complexity=62 - --max-line-length=127 - --statistics - - --per-file-ignores=datajoint/diagram.py:C901 files: src/ # a lot of files in tests are not compliant - repo: https://github.com/rhysd/actionlint rev: v1.7.7 From b00a4f0d7d5614a7d416b28503d2025586d3b6b2 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Dec 2025 17:49:45 +0100 Subject: [PATCH 025/219] more linting --- docs/src/api/make_pages.py | 4 +-- docs/src/tutorials/dj-top.ipynb | 52 +++++++------------------------- src/datajoint/autopopulate.py | 4 +-- src/datajoint/diagram.py | 2 +- src/datajoint/expression.py | 6 ++-- src/datajoint/external.py | 4 +-- src/datajoint/fetch.py | 6 ++-- src/datajoint/heading.py | 2 +- src/datajoint/schemas.py | 14 ++++----- src/datajoint/table.py | 17 +++++------ tests/test_admin.py | 2 +- tests/test_aggr_regressions.py | 4 +-- tests/test_cli.py | 4 +-- tests/test_declare.py | 15 ++++++++- tests/test_erd.py | 1 - tests/test_fetch.py | 12 ++++---- tests/test_foreign_keys.py | 8 ++++- tests/test_privileges.py | 7 +++-- tests/test_relation_u.py | 10 +++--- tests/test_relational_operand.py | 12 ++++---- tests/test_schema.py | 2 +- tests/test_settings.py | 2 +- tests/test_university.py | 13 +++++++- tests/test_uuid.py | 2 +- 24 files changed, 102 insertions(+), 103 deletions(-) diff --git a/docs/src/api/make_pages.py b/docs/src/api/make_pages.py index 3072cb46a..25dc29943 100644 --- a/docs/src/api/make_pages.py +++ b/docs/src/api/make_pages.py @@ -9,9 +9,7 @@ nav = mkdocs_gen_files.Nav() for path in sorted(Path(package).glob("**/*.py")): with mkdocs_gen_files.open(f"api/{path.with_suffix('')}.md", "w") as f: - module_path = ".".join( - [p for p in path.with_suffix("").parts if p != "__init__"] - ) + module_path = ".".join([p for p in path.with_suffix("").parts if p != "__init__"]) print(f"::: {module_path}", file=f) nav[path.parts] = f"{path.with_suffix('')}.md" diff --git a/docs/src/tutorials/dj-top.ipynb b/docs/src/tutorials/dj-top.ipynb index 7ed9f97cc..5920a9f25 100644 --- a/docs/src/tutorials/dj-top.ipynb +++ b/docs/src/tutorials/dj-top.ipynb @@ -229,9 +229,7 @@ " home_city=city,\n", " home_state=state,\n", " home_zip=zipcode,\n", - " date_of_birth=str(\n", - " fake.date_time_between(start_date=\"-35y\", end_date=\"-15y\").date()\n", - " ),\n", + " date_of_birth=str(fake.date_time_between(start_date=\"-35y\", end_date=\"-15y\").date()),\n", " home_phone=fake.phone_number()[:20],\n", " )" ] @@ -261,9 +259,7 @@ "\n", "StudentMajor.insert(\n", " {**s, **d, \"declare_date\": fake.date_between(start_date=datetime.date(1999, 1, 1))}\n", - " for s, d in zip(\n", - " Student.fetch(\"KEY\"), random.choices(Department.fetch(\"KEY\"), k=len(Student()))\n", - " )\n", + " for s, d in zip(Student.fetch(\"KEY\"), random.choices(Department.fetch(\"KEY\"), k=len(Student())))\n", " if random.random() < 0.75\n", ")\n", "\n", @@ -318,17 +314,11 @@ " ]\n", ")\n", "\n", - "Term.insert(\n", - " dict(term_year=year, term=term)\n", - " for year in range(1999, 2019)\n", - " for term in [\"Spring\", \"Summer\", \"Fall\"]\n", - ")\n", + "Term.insert(dict(term_year=year, term=term) for year in range(1999, 2019) for term in [\"Spring\", \"Summer\", \"Fall\"])\n", "\n", "Term().fetch(order_by=(\"term_year DESC\", \"term DESC\"), as_dict=True, limit=1)[0]\n", "\n", - "CurrentTerm().insert1(\n", - " {**Term().fetch(order_by=(\"term_year DESC\", \"term DESC\"), as_dict=True, limit=1)[0]}\n", - ")\n", + "CurrentTerm().insert1({**Term().fetch(order_by=(\"term_year DESC\", \"term DESC\"), as_dict=True, limit=1)[0]})\n", "\n", "\n", "def make_section(prob):\n", @@ -372,10 +362,7 @@ " sections = ((Section & term) - (Course & (Enroll & student))).fetch(\"KEY\")\n", " if sections:\n", " Enroll.insert(\n", - " {**student, **section}\n", - " for section in random.sample(\n", - " sections, random.randrange(min(5, len(sections)))\n", - " )\n", + " {**student, **section} for section in random.sample(sections, random.randrange(min(5, len(sections))))\n", " )\n", "\n", "# assign random grades\n", @@ -385,10 +372,7 @@ "random.shuffle(grade_keys)\n", "grade_keys = grade_keys[: len(grade_keys) * 9 // 10]\n", "\n", - "Grade.insert(\n", - " {**key, \"grade\": grade}\n", - " for key, grade in zip(grade_keys, random.choices(grades, k=len(grade_keys)))\n", - ")" + "Grade.insert({**key, \"grade\": grade} for key, grade in zip(grade_keys, random.choices(grades, k=len(grade_keys))))" ] }, { @@ -544,9 +528,7 @@ } ], "source": [ - "(Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(\n", - " limit=5, order_by=\"points DESC\", offset=5\n", - ")" + "(Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(limit=5, order_by=\"points DESC\", offset=5)" ] }, { @@ -566,11 +548,7 @@ } ], "source": [ - "(\n", - " (LetterGrade * Grade)\n", - " & \"term_year='2018'\"\n", - " & dj.Top(limit=10, order_by=\"points DESC\", offset=0)\n", - ").make_sql()" + "((LetterGrade * Grade) & \"term_year='2018'\" & dj.Top(limit=10, order_by=\"points DESC\", offset=0)).make_sql()" ] }, { @@ -590,11 +568,7 @@ } ], "source": [ - "(\n", - " (Grade * LetterGrade)\n", - " & \"term_year='2018'\"\n", - " & dj.Top(limit=20, order_by=\"points DESC\", offset=0)\n", - ").make_sql()" + "((Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(limit=20, order_by=\"points DESC\", offset=0)).make_sql()" ] }, { @@ -800,9 +774,7 @@ } ], "source": [ - "(Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(\n", - " limit=20, order_by=\"points DESC\", offset=0\n", - ")" + "(Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(limit=20, order_by=\"points DESC\", offset=0)" ] }, { @@ -1008,9 +980,7 @@ } ], "source": [ - "(LetterGrade * Grade) & \"term_year='2018'\" & dj.Top(\n", - " limit=20, order_by=\"points DESC\", offset=0\n", - ")" + "(LetterGrade * Grade) & \"term_year='2018'\" & dj.Top(limit=20, order_by=\"points DESC\", offset=0)" ] }, { diff --git a/src/datajoint/autopopulate.py b/src/datajoint/autopopulate.py index 53e64beeb..677a8113c 100644 --- a/src/datajoint/autopopulate.py +++ b/src/datajoint/autopopulate.py @@ -76,7 +76,7 @@ def _rename_attributes(table, props): if self._key_source is None: parents = self.target.parents(primary=True, as_objects=True, foreign_key_info=True) if not parents: - raise DataJointError("A table must have dependencies " "from its primary key for auto-populate to work") + raise DataJointError("A table must have dependencies from its primary key for auto-populate to work") self._key_source = _rename_attributes(*parents[0]) for q in parents[1:]: self._key_source *= _rename_attributes(*q) @@ -174,7 +174,7 @@ def _jobs_to_do(self, restrictions): """ if self.restriction: raise DataJointError( - "Cannot call populate on a restricted table. " "Instead, pass conditions to populate() as arguments." + "Cannot call populate on a restricted table. Instead, pass conditions to populate() as arguments." ) todo = self.key_source diff --git a/src/datajoint/diagram.py b/src/datajoint/diagram.py index c398b065f..3b6061102 100644 --- a/src/datajoint/diagram.py +++ b/src/datajoint/diagram.py @@ -361,7 +361,7 @@ def make_dot(self): dest = edge.get_destination() props = graph.get_edge_data(src, dest) if props is None: - raise DataJointError("Could not find edge with source " "'{}' and destination '{}'".format(src, dest)) + raise DataJointError("Could not find edge with source '{}' and destination '{}'".format(src, dest)) edge.set_color("#00000040") edge.set_style("solid" if props["primary"] else "dashed") master_part = graph.nodes[dest]["node_type"] is Part and dest.startswith(src + ".") diff --git a/src/datajoint/expression.py b/src/datajoint/expression.py index b64cf070f..17d529ff8 100644 --- a/src/datajoint/expression.py +++ b/src/datajoint/expression.py @@ -358,9 +358,9 @@ def proj(self, *attributes, **named_attributes): """ named_attributes = {k: translate_attribute(v)[1] for k, v in named_attributes.items()} # new attributes in parentheses are included again with the new name without removing original - duplication_pattern = re.compile(rf'^\s*\(\s*(?!{"|".join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*\)\s*$') + duplication_pattern = re.compile(rf"^\s*\(\s*(?!{'|'.join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*\)\s*$") # attributes without parentheses renamed - rename_pattern = re.compile(rf'^\s*(?!{"|".join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*$') + rename_pattern = re.compile(rf"^\s*(?!{'|'.join(CONSTANT_LITERALS)})(?P[a-zA-Z_]\w*)\s*$") replicate_map = { k: m.group("name") for k, m in ((k, duplication_pattern.match(v)) for k, v in named_attributes.items()) if m } @@ -562,7 +562,7 @@ def __next__(self): key = self._iter_keys.pop(0) except AttributeError: # self._iter_keys is missing because __iter__ has not been called. - raise TypeError("A QueryExpression object is not an iterator. " "Use iter(obj) to create an iterator.") + raise TypeError("A QueryExpression object is not an iterator. Use iter(obj) to create an iterator.") except IndexError: raise StopIteration else: diff --git a/src/datajoint/external.py b/src/datajoint/external.py index 583ef24e4..3f9efcf8e 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -171,7 +171,7 @@ def put(self, blob): self._upload_buffer(blob, self._make_uuid_path(uuid)) # insert tracking info self.connection.query( - "INSERT INTO {tab} (hash, size) VALUES (%s, {size}) ON DUPLICATE KEY " "UPDATE timestamp=CURRENT_TIMESTAMP".format( + "INSERT INTO {tab} (hash, size) VALUES (%s, {size}) ON DUPLICATE KEY UPDATE timestamp=CURRENT_TIMESTAMP".format( tab=self.full_table_name, size=len(blob) ), args=(uuid.bytes,), @@ -394,7 +394,7 @@ def delete( :return: if deleting external files, returns errors """ if delete_external_files not in (True, False): - raise DataJointError("The delete_external_files argument must be set to either " "True or False in delete()") + raise DataJointError("The delete_external_files argument must be set to either True or False in delete()") if not delete_external_files: self.unused().delete_quick() diff --git a/src/datajoint/fetch.py b/src/datajoint/fetch.py index 278a9c3f2..5d02b52b0 100644 --- a/src/datajoint/fetch.py +++ b/src/datajoint/fetch.py @@ -160,16 +160,16 @@ def __call__( # format should not be specified with attrs or is_dict=True if format is not None and (as_dict or attrs): raise DataJointError( - "Cannot specify output format when as_dict=True or " "when attributes are selected to be fetched separately." + "Cannot specify output format when as_dict=True or when attributes are selected to be fetched separately." ) if format not in {None, "array", "frame"}: - raise DataJointError("Fetch output format must be in " '{{"array", "frame"}} but "{}" was given'.format(format)) + raise DataJointError('Fetch output format must be in {{"array", "frame"}} but "{}" was given'.format(format)) if not (attrs or as_dict) and format is None: format = config["fetch_format"] # default to array if format not in {"array", "frame"}: raise DataJointError( - 'Invalid entry "{}" in datajoint.config["fetch_format"]: ' 'use "array" or "frame"'.format(format) + 'Invalid entry "{}" in datajoint.config["fetch_format"]: use "array" or "frame"'.format(format) ) get = partial( diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index fcc21e019..45e35998c 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -308,7 +308,7 @@ def _init_from_database(self): "#migration-between-datajoint-v0-11-and-v0-12" ) raise DataJointError( - "Legacy datatype `{type}`. Migrate your external stores to " "datajoint 0.12: {url}".format( + "Legacy datatype `{type}`. Migrate your external stores to datajoint 0.12: {url}".format( url=url, **attr ) ) diff --git a/src/datajoint/schemas.py b/src/datajoint/schemas.py index 095e6fdc6..e9b83efff 100644 --- a/src/datajoint/schemas.py +++ b/src/datajoint/schemas.py @@ -124,7 +124,7 @@ def activate( if not self.exists: if not self.create_schema or not self.database: raise DataJointError( - "Database `{name}` has not yet been declared. " "Set argument create_schema=True to create it.".format( + "Database `{name}` has not yet been declared. Set argument create_schema=True to create it.".format( name=schema_name ) ) @@ -134,7 +134,7 @@ def activate( self.connection.query("CREATE DATABASE `{name}`".format(name=schema_name)) except AccessError: raise DataJointError( - "Schema `{name}` does not exist and could not be created. " "Check permissions.".format(name=schema_name) + "Schema `{name}` does not exist and could not be created. Check permissions.".format(name=schema_name) ) else: self.log("created") @@ -220,7 +220,7 @@ def _decorate_table(self, table_class, context, assert_declared=False): if len(contents) > len(instance): if instance.heading.has_autoincrement: warnings.warn( - ("Contents has changed but cannot be inserted because " "{table} has autoincrement.").format( + ("Contents has changed but cannot be inserted because {table} has autoincrement.").format( table=instance.__class__.__name__ ) ) @@ -317,7 +317,7 @@ def drop(self, force=False): logger.debug("Schema `{database}` was dropped successfully.".format(database=self.database)) except AccessError: raise AccessError( - "An attempt to drop schema `{database}` " "has failed. Check permissions.".format(database=self.database) + "An attempt to drop schema `{database}` has failed. Check permissions.".format(database=self.database) ) @property @@ -329,7 +329,7 @@ def exists(self): raise DataJointError("Schema must be activated first.") return bool( self.connection.query( - "SELECT schema_name " "FROM information_schema.schemata " "WHERE schema_name = '{database}'".format( + "SELECT schema_name FROM information_schema.schemata WHERE schema_name = '{database}'".format( database=self.database ) ).rowcount @@ -382,7 +382,7 @@ def replace(s): ) return ("" if tier == "Part" else "\n@schema\n") + ( - "{indent}class {class_name}(dj.{tier}):\n" '{indent} definition = """\n' '{indent} {defi}"""' + '{indent}class {class_name}(dj.{tier}):\n{indent} definition = """\n{indent} {defi}"""' ).format( class_name=class_name, indent=indent, @@ -476,6 +476,6 @@ def list_schemas(connection=None): return [ r[0] for r in (connection or conn()).query( - "SELECT schema_name " "FROM information_schema.schemata " 'WHERE schema_name <> "information_schema"' + 'SELECT schema_name FROM information_schema.schemata WHERE schema_name <> "information_schema"' ) ] diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 94b140797..9cd63b9e0 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -93,7 +93,7 @@ def declare(self, context=None): not allowed. """ if self.connection.in_transaction: - raise DataJointError("Cannot declare new tables inside a transaction, " "e.g. from inside a populate/make call") + raise DataJointError("Cannot declare new tables inside a transaction, e.g. from inside a populate/make call") # Enforce strict CamelCase #1150 if not is_camel_case(self.class_name): raise DataJointError( @@ -118,9 +118,7 @@ def alter(self, prompt=True, context=None): Alter the table definition from self.definition """ if self.connection.in_transaction: - raise DataJointError( - "Cannot update table declaration inside a transaction, " "e.g. from inside a populate/make call" - ) + raise DataJointError("Cannot update table declaration inside a transaction, e.g. from inside a populate/make call") if context is None: frame = inspect.currentframe().f_back context = dict(frame.f_globals, **frame.f_locals) @@ -569,7 +567,7 @@ def cascade(table): if transaction: self.connection.cancel_transaction() raise DataJointError( - "Attempt to delete part table {part} before deleting from " "its master {master} first.".format( + "Attempt to delete part table {part} before deleting from its master {master} first.".format( part=part, master=master ) ) @@ -614,7 +612,7 @@ def drop(self): """ if self.restriction: raise DataJointError( - "A table with an applied restriction cannot be dropped." " Call drop() on the unrestricted Table." + "A table with an applied restriction cannot be dropped. Call drop() on the unrestricted Table." ) self.connection.dependencies.load() do_drop = True @@ -625,7 +623,7 @@ def drop(self): master = get_master(part) if master and master not in tables: raise DataJointError( - "Attempt to drop part table {part} before dropping " "its master. Drop {master} first.".format( + "Attempt to drop part table {part} before dropping its master. Drop {master} first.".format( part=part, master=master ) ) @@ -799,8 +797,9 @@ def check_fields(fields): try: if len(row) != len(self.heading): raise DataJointError( - "Invalid insert argument. Incorrect number of attributes: " - "{given} given; {expected} expected".format(given=len(row), expected=len(self.heading)) + "Invalid insert argument. Incorrect number of attributes: {given} given; {expected} expected".format( + given=len(row), expected=len(self.heading) + ) ) except TypeError: raise DataJointError("Datatype %s cannot be inserted" % type(row)) diff --git a/tests/test_admin.py b/tests/test_admin.py index b600b21e4..8625fd24d 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -24,7 +24,7 @@ def user_alice(db_creds_root) -> dict: password="oldpass", ) root_conn.query(f"DROP USER IF EXISTS '{new_credentials['user']}'@'%%';") - root_conn.query(f"CREATE USER '{new_credentials['user']}'@'%%' " f"IDENTIFIED BY '{new_credentials['password']}';") + root_conn.query(f"CREATE USER '{new_credentials['user']}'@'%%' IDENTIFIED BY '{new_credentials['password']}';") # test the connection dj.Connection(**new_credentials) diff --git a/tests/test_aggr_regressions.py b/tests/test_aggr_regressions.py index afbcdda18..a45259867 100644 --- a/tests/test_aggr_regressions.py +++ b/tests/test_aggr_regressions.py @@ -70,8 +70,8 @@ def test_issue484(schema_aggr_reg): Issue 484 """ q = dj.U().aggr(S, n="max(s)") - n = q.fetch("n") - n = q.fetch1("n") + q.fetch("n") + q.fetch1("n") q = dj.U().aggr(S, n="avg(s)") result = dj.U().aggr(q, m="max(n)") result.fetch() diff --git a/tests/test_cli.py b/tests/test_cli.py index 2fb86d796..8e0660c13 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,7 +12,7 @@ def test_cli_version(capsys): with pytest.raises(SystemExit) as pytest_wrapped_e: dj.cli(args=["-V"]) - assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.type is SystemExit assert pytest_wrapped_e.value.code == 0 captured_output = capsys.readouterr().out @@ -22,7 +22,7 @@ def test_cli_version(capsys): def test_cli_help(capsys): with pytest.raises(SystemExit) as pytest_wrapped_e: dj.cli(args=["--help"]) - assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.type is SystemExit assert pytest_wrapped_e.value.code == 0 captured_output = capsys.readouterr().out diff --git a/tests/test_declare.py b/tests/test_declare.py index 21083ea2a..5f8d6497d 100644 --- a/tests/test_declare.py +++ b/tests/test_declare.py @@ -6,7 +6,20 @@ from datajoint.declare import declare from datajoint.settings import config -from .schema import Auto, Ephys, Experiment, IndexRich, Subject, TTest, TTest2, ThingC, Trial, User +from .schema import ( + Auto, + Ephys, + Experiment, + IndexRich, + Subject, + TTest, + TTest2, + ThingA, # noqa: F401 - needed in globals for foreign key resolution + ThingB, # noqa: F401 - needed in globals for foreign key resolution + ThingC, + Trial, + User, +) @pytest.fixture(scope="function") diff --git a/tests/test_erd.py b/tests/test_erd.py index 9bf59c334..8c12b1f6b 100644 --- a/tests/test_erd.py +++ b/tests/test_erd.py @@ -1,6 +1,5 @@ import datajoint as dj -from .schema_advanced import * from .schema_simple import LOCALS_SIMPLE, A, B, D, E, G, L diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 26a9229a5..0ebe297d9 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -45,7 +45,7 @@ def test_order_by(lang, languages): cur = lang.fetch(order_by=("name " + ord_name, "language " + ord_lang)) languages.sort(key=itemgetter(1), reverse=ord_lang == "DESC") languages.sort(key=itemgetter(0), reverse=ord_name == "DESC") - for c, l in zip(cur, languages): + for c, l in zip(cur, languages): # noqa: E741 assert np.all(cc == ll for cc, ll in zip(c, l)), "Sorting order is different" @@ -54,7 +54,7 @@ def test_order_by_default(lang, languages): cur = lang.fetch(order_by=("language", "name DESC")) languages.sort(key=itemgetter(0), reverse=True) languages.sort(key=itemgetter(1), reverse=False) - for c, l in zip(cur, languages): + for c, l in zip(cur, languages): # noqa: E741 assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" @@ -71,7 +71,7 @@ def test_order_by_limit(lang, languages): languages.sort(key=itemgetter(0), reverse=True) languages.sort(key=itemgetter(1), reverse=False) assert len(cur) == 4, "Length is not correct" - for c, l in list(zip(cur, languages))[:4]: + for c, l in list(zip(cur, languages))[:4]: # noqa: E741 assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" @@ -99,7 +99,7 @@ def test_limit_offset(lang, languages): languages.sort(key=itemgetter(0), reverse=True) languages.sort(key=itemgetter(1), reverse=False) assert len(cur) == 4, "Length is not correct" - for c, l in list(zip(cur, languages[2:6])): + for c, l in list(zip(cur, languages[2:6])): # noqa: E741 assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" @@ -161,7 +161,7 @@ def test_fetch1_step1(lang, languages): def test_misspelled_attribute(schema_any): with pytest.raises(dj.DataJointError): - f = (schema.Language & 'lang = "ENGLISH"').fetch() + (schema.Language & 'lang = "ENGLISH"').fetch() def test_repr(subject): @@ -193,7 +193,7 @@ def test_offset(lang, languages): languages.sort(key=itemgetter(0), reverse=True) languages.sort(key=itemgetter(1), reverse=False) assert len(cur) == 4, "Length is not correct" - for c, l in list(zip(cur, languages[1:]))[:4]: + for c, l in list(zip(cur, languages[1:]))[:4]: # noqa: E741 assert np.all([cc == ll for cc, ll in zip(c, l)]), "Sorting order is different" diff --git a/tests/test_foreign_keys.py b/tests/test_foreign_keys.py index 6049bd53f..e7f9d62c6 100644 --- a/tests/test_foreign_keys.py +++ b/tests/test_foreign_keys.py @@ -1,6 +1,12 @@ from datajoint.declare import declare -from .schema_advanced import * +from .schema_advanced import ( + Cell, # noqa: F401 - needed in globals for foreign key resolution + GlobalSynapse, + LocalSynapse, + Parent, + Person, +) def test_aliased_fk(schema_adv): diff --git a/tests/test_privileges.py b/tests/test_privileges.py index ed5925963..4a338eaec 100644 --- a/tests/test_privileges.py +++ b/tests/test_privileges.py @@ -82,9 +82,12 @@ def test_fail_create_schema(self, connection_djview): def test_insert_failure(self, connection_djview, schema_any): unprivileged = dj.Schema(schema_any.database, namespace, connection=connection_djview) unprivileged.spawn_missing_classes() - assert issubclass(Language, dj.Lookup) and len(Language()) == len(schema.Language()), "failed to spawn missing classes" + UnprivilegedLanguage = namespace["Language"] + assert issubclass(UnprivilegedLanguage, dj.Lookup) and len(UnprivilegedLanguage()) == len( + schema.Language() + ), "failed to spawn missing classes" with pytest.raises(dj.DataJointError): - Language().insert1(("Socrates", "Greek")) + UnprivilegedLanguage().insert1(("Socrates", "Greek")) def test_failure_to_create_table(self, connection_djview, schema_any): unprivileged = dj.Schema(schema_any.database, namespace, connection=connection_djview) diff --git a/tests/test_relation_u.py b/tests/test_relation_u.py index 67304d36e..109251d2c 100644 --- a/tests/test_relation_u.py +++ b/tests/test_relation_u.py @@ -2,8 +2,8 @@ import datajoint as dj -from .schema import * -from .schema_simple import * +from .schema import Language, TTest +from .schema_simple import ArgmaxTest def test_restriction(lang, languages, trial): @@ -21,7 +21,7 @@ def test_restriction(lang, languages, trial): def test_invalid_restriction(schema_any): with raises(dj.DataJointError): - result = dj.U("color") & dict(color="red") + dj.U("color") & dict(color="red") def test_ineffective_restriction(lang): @@ -41,7 +41,7 @@ def test_join(experiment): def test_invalid_join(schema_any): with raises(dj.DataJointError): - rel = dj.U("language") * dict(language="English") + dj.U("language") * dict(language="English") def test_repr_without_attrs(schema_any): @@ -59,7 +59,7 @@ def test_aggregations(schema_any): n2 = dj.U().aggr(Language, n="count(*)").fetch1("n") assert n1 == n2 rel = dj.U("language").aggr(Language, number_of_speakers="count(*)") - assert len(rel) == len(set(l[1] for l in Language.contents)) + assert len(rel) == len(set(lang[1] for lang in Language.contents)) assert (rel & 'language="English"').fetch1("number_of_speakers") == 3 diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py index 04b364b02..371dc03e6 100644 --- a/tests/test_relational_operand.py +++ b/tests/test_relational_operand.py @@ -9,8 +9,8 @@ import datajoint as dj from datajoint.errors import DataJointError -from .schema import * -from .schema_simple import * +from .schema import Child, Ephys, Experiment, Parent, SessionA, SessionDateA, SessionStatusA, SubjectA, TTest3, Trial +from .schema_simple import F, IJ, JI, L, A, B, D, E, DataA, DataB, KeyPK, OutfitLaunch, ReservedWord, SelectPK, TTestUpdate @pytest.fixture @@ -161,7 +161,7 @@ def test_project(schema_simp_pop): # projection after restriction cond = L() & "cond_in_l" assert len(D() & cond) + len(D() - cond) == len(D()), "failed semijoin or antijoin" - assert len((D() & cond).proj()) == len((D() & cond)), "projection failed: altered its argument" "s cardinality" + assert len((D() & cond).proj()) == len((D() & cond)), "projection failed: altered its arguments cardinality" def test_rename_non_dj_attribute(connection_test, schema_simp_pop, schema_any_pop, prefix): @@ -183,13 +183,13 @@ def test_union(schema_simp_pop): assert len(IJ + JI) == len(z) -def test_outer_union_fail(schema_simp_pop): +def test_outer_union_fail_1(schema_simp_pop): """Union of two tables with different primary keys raises an error.""" with pytest.raises(dj.DataJointError): A() + B() -def test_outer_union_fail(schema_any_pop): +def test_outer_union_fail_2(schema_any_pop): """Union of two tables with different primary keys raises an error.""" t = Trial + Ephys t.fetch() @@ -367,7 +367,7 @@ def test_date(schema_simp_pop): assert (F & "id=2").fetch1("date") == new_value F.update1(dict((F & "id=2").fetch1("KEY"), date=None)) - assert (F & "id=2").fetch1("date") == None + assert (F & "id=2").fetch1("date") is None def test_join_project(schema_simp_pop): diff --git a/tests/test_schema.py b/tests/test_schema.py index 43bb7baf3..263b64e91 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -226,7 +226,7 @@ class Subject(dj.Manual): name: varchar(32) """ - _ = dj.VirtualModule("Schema_A", "Schema_A") + Schema_A = dj.VirtualModule("Schema_A", "Schema_A") # noqa: F841 schema2 = dj.Schema("schema_b") diff --git a/tests/test_settings.py b/tests/test_settings.py index 23c3e5280..3a91719a8 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -81,7 +81,7 @@ def test_save(): os.rename(tmpfile, settings.LOCALCONFIG) -def test_load_save(): +def test_load_save_2(): """Testing load and save of config""" filename_old = dj.settings.LOCALCONFIG filename = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(50)) + ".json" diff --git a/tests/test_university.py b/tests/test_university.py index ec2ee6cdd..640884a9b 100644 --- a/tests/test_university.py +++ b/tests/test_university.py @@ -7,7 +7,18 @@ from datajoint import DataJointError from . import schema_university -from .schema_university import * +from .schema_university import ( + Student, + Department, + StudentMajor, + Course, + Term, + Section, + CurrentTerm, + Enroll, + LetterGrade, + Grade, +) def _hash4(table): diff --git a/tests/test_uuid.py b/tests/test_uuid.py index 4392e4769..0d4e2fb25 100644 --- a/tests/test_uuid.py +++ b/tests/test_uuid.py @@ -49,7 +49,7 @@ def test_invalid_uuid_restrict1(schema_uuid): k, m = (Basic & {"item": u}).fetch1("KEY", "number") -def test_invalid_uuid_restrict1(schema_uuid): +def test_invalid_uuid_restrict2(schema_uuid): """test that only UUID objects are accepted when inserting UUID fields""" u = "abc" with pytest.raises(DataJointError): From 65b701a81c8f399e7fe9af7d73d0a15e4e5c67a1 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Dec 2025 17:53:54 +0100 Subject: [PATCH 026/219] sync pyproject.toml --- pyproject.toml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a12269a97..f9518edd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,3 +145,37 @@ skip = ".git,*.pdf,*.svg,*.csv,*.ipynb,*.drawio" # numer -- numerator variable # astroid -- Python library name (not "asteroid") ignore-words-list = "rever,numer,astroid" + +[tool.pytest_env] +# Default values - pytest fixtures will override with actual container details +DJ_USER="root" +DJ_PASS="password" +DJ_TEST_USER="datajoint" +DJ_TEST_PASSWORD="datajoint" +S3_ACCESS_KEY="datajoint" +S3_SECRET_KEY="datajoint" +S3_BUCKET="datajoint.test" +PYTHON_USER="dja" +JUPYTER_PASSWORD="datajoint" + + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64", "osx-arm64", "linux-aarch64"] + +[tool.pixi.pypi-dependencies] +datajoint = { path = ".", editable = true } + +[tool.pixi.environments] +default = { solve-group = "default" } +dev = { features = ["dev"], solve-group = "default" } +test = { features = ["test"], solve-group = "default" } + +[tool.pixi.tasks] + +[tool.pixi.dependencies] +python = ">=3.9,<3.14" +graphviz = ">=13.1.2,<14" + +[tool.pixi.activation] +scripts=["activate.sh"] \ No newline at end of file From 908d226b888d093624c455e8514db582a43491c9 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 12 Dec 2025 17:59:51 +0100 Subject: [PATCH 027/219] fix long matlab blobs --- pyproject.toml | 1 + tests/test_blob_matlab.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f9518edd1..76fc5aed7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ ignore = [ # Per-file ignores (equivalent to flake8 --per-file-ignores) [tool.ruff.lint.per-file-ignores] "datajoint/diagram.py" = ["C901"] # function too complex +"tests/test_blob_matlab.py" = ["E501"] # SQL hex strings cannot be broken across lines [tool.ruff.lint.mccabe] # Maximum complexity (equivalent to flake8 --max-complexity) diff --git a/tests/test_blob_matlab.py b/tests/test_blob_matlab.py index 80a005659..09676090b 100644 --- a/tests/test_blob_matlab.py +++ b/tests/test_blob_matlab.py @@ -36,13 +36,13 @@ def insert_blobs(schema): schema.connection.query( """ INSERT INTO {table_name} (`id`, `comment`, `blob`) VALUES - (1,'simple string',0x6D596D00410200000000000000010000000000000010000000000000000400000000000000630068006100720061006300740065007200200073007400720069006E006700), - (2,'1D vector',0x6D596D0041020000000000000001000000000000000C000000000000000600000000000000000000000000F03F00000000000030400000000000003F4000000000000047400000000000804E4000000000000053400000000000C056400000000000805A400000000000405E4000000000000061400000000000E062400000000000C06440), - (3,'string array',0x6D596D00430200000000000000010000000000000002000000000000002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E00670031002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E0067003200), - (4,'struct array',0x6D596D005302000000000000000100000000000000020000000000000002000000610062002900000000000000410200000000000000010000000000000001000000000000000600000000000000000000000000F03F9000000000000000530200000000000000010000000000000001000000000000000100000063006900000000000000410200000000000000030000000000000003000000000000000600000000000000000000000000204000000000000008400000000000001040000000000000F03F0000000000001440000000000000224000000000000018400000000000001C40000000000000004029000000000000004102000000000000000100000000000000010000000000000006000000000000000000000000000040100100000000000053020000000000000001000000000000000100000000000000010000004300E9000000000000004102000000000000000500000000000000050000000000000006000000000000000000000000003140000000000000374000000000000010400000000000002440000000000000264000000000000038400000000000001440000000000000184000000000000028400000000000003240000000000000F03F0000000000001C400000000000002A400000000000003340000000000000394000000000000020400000000000002C400000000000003440000000000000354000000000000000400000000000002E400000000000003040000000000000364000000000000008400000000000002240), - (5,'3D double array',0x6D596D004103000000000000000200000000000000030000000000000004000000000000000600000000000000000000000000F03F000000000000004000000000000008400000000000001040000000000000144000000000000018400000000000001C40000000000000204000000000000022400000000000002440000000000000264000000000000028400000000000002A400000000000002C400000000000002E40000000000000304000000000000031400000000000003240000000000000334000000000000034400000000000003540000000000000364000000000000037400000000000003840), - (6,'3D uint8 array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000009000000000000000102030405060708090A0B0C0D0E0F101112131415161718), - (7,'3D complex array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000006000000010000000000000000C0724000000000000028C000000000000038C0000000000000000000000000000038C0000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000AA4C58E87AB62B400000000000000000AA4C58E87AB62BC0000000000000008000000000000052400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000008000000000000052C000000000000000800000000000000080000000000000008000000000000000800000000000000080 + (1,'simple string',0x6D596D00410200000000000000010000000000000010000000000000000400000000000000630068006100720061006300740065007200200073007400720069006E006700), # noqa: E501 + (2,'1D vector',0x6D596D0041020000000000000001000000000000000C000000000000000600000000000000000000000000F03F00000000000030400000000000003F4000000000000047400000000000804E4000000000000053400000000000C056400000000000805A400000000000405E4000000000000061400000000000E062400000000000C06440), # noqa: E501 + (3,'string array',0x6D596D00430200000000000000010000000000000002000000000000002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E00670031002F0000000000000041020000000000000001000000000000000700000000000000040000000000000073007400720069006E0067003200), # noqa: E501 + (4,'struct array',0x6D596D005302000000000000000100000000000000020000000000000002000000610062002900000000000000410200000000000000010000000000000001000000000000000600000000000000000000000000F03F9000000000000000530200000000000000010000000000000001000000000000000100000063006900000000000000410200000000000000030000000000000003000000000000000600000000000000000000000000204000000000000008400000000000001040000000000000F03F0000000000001440000000000000224000000000000018400000000000001C40000000000000004029000000000000004102000000000000000100000000000000010000000000000006000000000000000000000000000040100100000000000053020000000000000001000000000000000100000000000000010000004300E9000000000000004102000000000000000500000000000000050000000000000006000000000000000000000000003140000000000000374000000000000010400000000000002440000000000000264000000000000038400000000000001440000000000000184000000000000028400000000000003240000000000000F03F0000000000001C400000000000002A400000000000003340000000000000394000000000000020400000000000002C400000000000003440000000000000354000000000000000400000000000002E400000000000003040000000000000364000000000000008400000000000002240), # noqa: E501 + (5,'3D double array',0x6D596D004103000000000000000200000000000000030000000000000004000000000000000600000000000000000000000000F03F000000000000004000000000000008400000000000001040000000000000144000000000000018400000000000001C40000000000000204000000000000022400000000000002440000000000000264000000000000028400000000000002A400000000000002C400000000000002E40000000000000304000000000000031400000000000003240000000000000334000000000000034400000000000003540000000000000364000000000000037400000000000003840), # noqa: E501 + (6,'3D uint8 array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000009000000000000000102030405060708090A0B0C0D0E0F101112131415161718), # noqa: E501 + (7,'3D complex array',0x6D596D0041030000000000000002000000000000000300000000000000040000000000000006000000010000000000000000C0724000000000000028C000000000000038C0000000000000000000000000000038C0000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052C00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000AA4C58E87AB62B400000000000000000AA4C58E87AB62BC0000000000000008000000000000052400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000008000000000000052C000000000000000800000000000000080000000000000008000000000000000800000000000000080 # noqa: E501 ); """.format(table_name=Blob.full_table_name) ) From ddae20dbdddbf3b0f510a2ab0e72cea24ac48ff1 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 17 Dec 2025 20:31:38 +0100 Subject: [PATCH 028/219] switch settings to use pydantic-settings --- pixi.lock | 259 ++++++++------- pyproject.toml | 1 + src/datajoint/settings.py | 676 +++++++++++++++++++++++++++++--------- 3 files changed, 672 insertions(+), 264 deletions(-) diff --git a/pixi.lock b/pixi.lock index e823b4f84..dcc82c2b5 100644 --- a/pixi.lock +++ b/pixi.lock @@ -97,6 +97,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl @@ -129,11 +130,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl @@ -141,6 +146,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl @@ -247,6 +253,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl @@ -279,11 +286,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl @@ -291,6 +302,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl @@ -352,6 +364,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl @@ -384,11 +397,15 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl @@ -396,6 +413,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl @@ -497,14 +515,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl @@ -515,20 +532,16 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/11/02ebebb09ff2104b690457cb7bc6ed700c9e0ce88cf581486bb0a5d3c88b/faker-37.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/9f/bf231c2a3fac99d1d7f1d89c76594f158693f981a4aa02be406e9f036832/fonttools-4.59.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/08/2a/5628a99d04acb2d2f2e749cdf4ea571d2575e898df0528a090948018b726/ipython-9.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/b8/9eea6630198cb303d131d95d285a024b3b8645b1763a2916fddb44ca8760/matplotlib-3.10.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9a/a5/bf3db6e66c4b160d6ea10b534c381a1955dfab34cb1017ea93aa33c70ed3/numpy-2.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl @@ -536,7 +549,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl @@ -545,25 +557,29 @@ environments: - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/b8/fbab973592e23ae313042d450fc26fa24282ebffba21ba373786e1ce63b4/pyparsing-3.2.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl @@ -671,14 +687,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/2e/7a/34c9402ad12bce609be4be1146a7d22a7fae8e9d752684b6315cce552a65/coverage-7.11.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl @@ -689,20 +704,16 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl @@ -710,7 +721,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl @@ -719,25 +729,29 @@ environments: - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl @@ -800,14 +814,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl @@ -818,20 +831,16 @@ environments: - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/48/c5/d5e07995077e48220269c28a221e168c91123ad5ceee44d548f54a057fc0/ipython-9.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/ae/f32695da4f93de50dd7075100dab8cf689a9d96270f58ce6f940fd044a3e/minio-7.2.18-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl @@ -839,7 +848,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl @@ -848,25 +856,29 @@ environments: - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl @@ -969,6 +981,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl @@ -1008,6 +1021,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl @@ -1016,6 +1032,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl @@ -1024,6 +1041,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl @@ -1130,6 +1148,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxxf86vm-1.1.6-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-xorgproto-2024.1-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-hbcf94c1_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl @@ -1169,6 +1188,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl @@ -1177,6 +1199,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl @@ -1185,6 +1208,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl @@ -1246,6 +1270,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl @@ -1285,6 +1310,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl @@ -1293,6 +1321,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/27/98/822b924a4a3eb58aacba84444c7439fce32680592f394de26af9c76e2569/pytest_env-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl @@ -1301,6 +1330,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl @@ -1364,6 +1394,13 @@ packages: purls: [] size: 631452 timestamp: 1758743294412 +- pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + name: annotated-types + version: 0.7.0 + sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.9' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl name: argon2-cffi version: 25.1.0 @@ -1507,25 +1544,6 @@ packages: purls: [] size: 347530 timestamp: 1713896411580 -- pypi: https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl - name: black - version: 24.2.0 - sha256: e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6 - requires_dist: - - click>=8.0.0 - - mypy-extensions>=0.4.3 - - packaging>=22.0 - - pathspec>=0.9.0 - - platformdirs>=2 - - tomli>=1.1.0 ; python_full_version < '3.11' - - typing-extensions>=4.0.1 ; python_full_version < '3.11' - - colorama>=0.4.3 ; extra == 'colorama' - - aiohttp>=3.7.4,!=3.9.0 ; implementation_name == 'pypy' and sys_platform == 'win32' and extra == 'd' - - aiohttp>=3.7.4 ; (implementation_name != 'pypy' and extra == 'd') or (sys_platform != 'win32' and extra == 'd') - - ipython>=7.8.0 ; extra == 'jupyter' - - tokenize-rt>=3.2.0 ; extra == 'jupyter' - - uvloop>=0.15.2 ; extra == 'uvloop' - requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 md5: 51a19bba1b8ebfb60df25cde030b7ebc @@ -1696,20 +1714,6 @@ packages: version: 3.4.4 sha256: e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl - name: click - version: 8.2.1 - sha256: 61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b - requires_dist: - - colorama ; sys_platform == 'win32' - requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl - name: click - version: 8.3.0 - sha256: 9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc - requires_dist: - - colorama ; sys_platform == 'win32' - requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl name: codespell version: 2.4.1 @@ -1845,7 +1849,7 @@ packages: - pypi: ./ name: datajoint version: 0.14.6 - sha256: 8da2585511ca6906c53e2fe4ecec75250c274eed754f2902bf5b69767ea006da + sha256: f761bb719d6afe0361d7e564bcc950ea76c79fbee9c334032459d0d4437a6423 requires_dist: - numpy - pymysql>=0.7.2 @@ -1861,10 +1865,9 @@ packages: - faker - urllib3 - setuptools + - pydantic-settings>=2.0.0 - pre-commit ; extra == 'dev' - - black==24.2.0 ; extra == 'dev' - - flake8 ; extra == 'dev' - - isort ; extra == 'dev' + - ruff ; extra == 'dev' - codespell ; extra == 'dev' - pytest ; extra == 'dev' - pytest-cov ; extra == 'dev' @@ -2041,15 +2044,6 @@ packages: version: 3.20.0 sha256: 339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl - name: flake8 - version: 7.3.0 - sha256: b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e - requires_dist: - - mccabe>=0.7.0,<0.8.0 - - pycodestyle>=2.14.0,<2.15.0 - - pyflakes>=3.4.0,<3.5.0 - requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b md5: 0c96522c6bdaed4b1566d11387caaf45 @@ -2965,22 +2959,6 @@ packages: requires_dist: - pygments requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl - name: isort - version: 6.0.1 - sha256: 2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615 - requires_dist: - - colorama ; extra == 'colors' - - setuptools ; extra == 'plugins' - requires_python: '>=3.9.0' -- pypi: https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl - name: isort - version: 7.0.0 - sha256: 1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 - requires_dist: - - colorama ; extra == 'colors' - - setuptools ; extra == 'plugins' - requires_python: '>=3.10.0' - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl name: jedi version: 0.19.2 @@ -4272,11 +4250,6 @@ packages: - notebook ; extra == 'test' - pytest ; extra == 'test' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl - name: mccabe - version: 0.7.0 - sha256: 6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e - requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl name: minio version: 7.2.16 @@ -4299,11 +4272,6 @@ packages: - typing-extensions - urllib3 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl - name: mypy-extensions - version: 1.1.0 - sha256: 1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 - requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 md5: 47e340acb35de30501a76c7c799c41d7 @@ -4788,11 +4756,6 @@ packages: - mypy==0.971 ; extra == 'qa' - types-setuptools==67.2.0.1 ; extra == 'qa' requires_python: '>=3.6' -- pypi: https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl - name: pathspec - version: 0.12.1 - sha256: a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 - requires_python: '>=3.8' - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.46-h1321c63_0.conda sha256: 5c7380c8fd3ad5fc0f8039069a45586aa452cf165264bc5a437ad80397b32934 md5: 7fa07cb0fb1b625a089ccc01218ee5b1 @@ -5069,11 +5032,6 @@ packages: sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 requires_dist: - pytest ; extra == 'tests' -- pypi: https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl - name: pycodestyle - version: 2.14.0 - sha256: dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl name: pycparser version: '2.23' @@ -5094,6 +5052,55 @@ packages: version: 3.23.0 sha256: 187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*' +- pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + name: pydantic + version: 2.12.5 + sha256: e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.41.5 + - typing-extensions>=4.14.1 + - typing-inspection>=0.4.2 + - email-validator>=2.0.0 ; extra == 'email' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: pydantic-core + version: 2.41.5 + sha256: 0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.41.5 + sha256: 112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: 406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl + name: pydantic-settings + version: 2.12.0 + sha256: fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809 + requires_dist: + - pydantic>=2.7.0 + - python-dotenv>=0.21.0 + - typing-inspection>=0.4.0 + - boto3-stubs[secretsmanager] ; extra == 'aws-secrets-manager' + - boto3>=1.35.0 ; extra == 'aws-secrets-manager' + - azure-identity>=1.16.0 ; extra == 'azure-key-vault' + - azure-keyvault-secrets>=4.8.0 ; extra == 'azure-key-vault' + - google-cloud-secret-manager>=2.23.1 ; extra == 'gcp-secret-manager' + - tomli>=2.0.1 ; extra == 'toml' + - pyyaml>=6.0.1 ; extra == 'yaml' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl name: pydot version: 4.0.1 @@ -5113,11 +5120,6 @@ packages: - pytest-xdist[psutil] ; extra == 'tests' - zest-releaser[recommended] ; extra == 'release' requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl - name: pyflakes - version: 3.4.0 - sha256: f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl name: pygments version: 2.19.2 @@ -5307,6 +5309,13 @@ packages: requires_dist: - six>=1.5 requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*' +- pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl + name: python-dotenv + version: 1.2.1 + sha256: b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 + requires_dist: + - click>=5.0 ; extra == 'cli' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda build_number: 8 sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 @@ -5381,6 +5390,21 @@ packages: - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/7d/f8/2be49047f929d6965401855461e697ab185e1a6a683d914c5c19c7962d9e/ruff-0.14.9-py3-none-macosx_11_0_arm64.whl + name: ruff + version: 0.14.9 + sha256: d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/9e/e9/08840ff5127916bb989c86f18924fd568938b06f58b60e206176f327c0fe/ruff-0.14.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: ruff + version: 0.14.9 + sha256: 84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f3/51/0489a6a5595b7760b5dbac0dd82852b510326e7d88d51dbffcd2e07e3ff3/ruff-0.14.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: ruff + version: 0.14.9 + sha256: 72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl name: setuptools version: 80.9.0 @@ -5526,6 +5550,13 @@ packages: version: 4.15.0 sha256: f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + name: typing-inspection + version: 0.4.2 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl name: tzdata version: '2025.2' diff --git a/pyproject.toml b/pyproject.toml index 76fc5aed7..74cf1ba5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "faker", "urllib3", "setuptools", + "pydantic-settings>=2.0.0", ] requires-python = ">=3.9,<3.14" authors = [ diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 9de7ae511..a7f07659b 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -1,5 +1,5 @@ """ -Settings for DataJoint +Settings for DataJoint using pydantic-settings """ import collections @@ -9,18 +9,73 @@ import pprint from contextlib import contextmanager from enum import Enum +from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple + +from pydantic import Field, field_validator +from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict from .errors import DataJointError + +class EnvSettingsSource(PydanticBaseSettingsSource): + """ + Custom settings source that reads legacy DJ_* environment variables. + + This is needed because DataJoint's historical environment variable names + don't follow pydantic's nested naming convention: + - DJ_HOST instead of DJ_DATABASE__HOST + - DJ_USER instead of DJ_DATABASE__USER + - DJ_PASS instead of DJ_DATABASE__PASSWORD + + This maintains backward compatibility with existing deployments. + """ + + def get_field_value(self, field: Any, field_name: str) -> Tuple[Any, str, bool]: + """Required abstract method - not used as we override __call__""" + return None, field_name, False + + def __call__(self) -> Dict[str, Any]: + d: Dict[str, Any] = {} + + # Read only DJ_* environment variables + env_mapping = { + "DJ_HOST": ("database", "host"), + "DJ_USER": ("database", "user"), + "DJ_PASS": ("database", "password"), + "DJ_PORT": ("database", "port"), + "DJ_AWS_ACCESS_KEY_ID": ("external", "aws_access_key_id"), + "DJ_AWS_SECRET_ACCESS_KEY": ("external", "aws_secret_access_key"), + "DJ_LOG_LEVEL": ("loglevel",), + } + + for env_var, path in env_mapping.items(): + env_value = os.getenv(env_var) + if env_value is not None: + if len(path) == 1: + # Top-level field + d[path[0]] = env_value + else: + # Nested field + if path[0] not in d: + d[path[0]] = {} + if path[0] == "database" and path[1] == "port": + # Convert port to integer + try: + d[path[0]][path[1]] = int(env_value) + except ValueError: + logger.warning(f"Invalid DJ_PORT value: {env_value}, using default") + else: + d[path[0]][path[1]] = env_value + + return d + + LOCALCONFIG = "dj_local_conf.json" GLOBALCONFIG = ".datajoint_config.json" # subfolding for external storage in filesystem. # 2, 2 means that file abcdef is stored as /ab/cd/abcdef DEFAULT_SUBFOLDING = (2, 2) -validators = collections.defaultdict(lambda: lambda value: True) -validators["database.port"] = lambda a: isinstance(a, int) - Role = Enum("Role", "manual lookup imported computed job") role_to_prefix = { Role.manual: "", @@ -31,29 +86,6 @@ } prefix_to_role = dict(zip(role_to_prefix.values(), role_to_prefix)) -default = dict( - { - "database.host": "localhost", - "database.password": None, - "database.user": None, - "database.port": 3306, - "database.reconnect": True, - "connection.init_function": None, - "connection.charset": "", # pymysql uses '' as default - "loglevel": "INFO", - "safemode": True, - "fetch_format": "array", - "display.limit": 12, - "display.width": 14, - "display.show_tuple_count": True, - "database.use_tls": None, - "enable_python_native_blobs": True, # python-native/dj0 encoding support - "add_hidden_timestamp": False, - # file size limit for when to disable checksums - "filepath_checksum_size_limit": None, - } -) - logger = logging.getLogger(__name__.split(".")[0]) log_levels = { "INFO": logging.INFO, @@ -65,40 +97,326 @@ } -class Config(collections.abc.MutableMapping): - instance = None +class DatabaseSettings(BaseSettings): + """Database connection settings""" - def __init__(self, *args, **kwargs): - if not Config.instance: - Config.instance = Config.__Config(*args, **kwargs) - else: - Config.instance._conf.update(dict(*args, **kwargs)) + host: str = "localhost" + password: Optional[str] = None + user: Optional[str] = None + port: int = 3306 + reconnect: bool = True + use_tls: Optional[bool] = None - def __getattr__(self, name): - return getattr(self.instance, name) + model_config = SettingsConfigDict( + env_prefix="DJ_", + case_sensitive=False, + extra="allow", + ) - def __getitem__(self, item): - return self.instance.__getitem__(item) - def __setitem__(self, item, value): - self.instance.__setitem__(item, value) +class ConnectionSettings(BaseSettings): + """Connection settings""" - def __str__(self): - return pprint.pformat(self.instance._conf, indent=4) + init_function: Optional[str] = None + charset: str = "" # pymysql uses '' as default + + model_config = SettingsConfigDict( + env_prefix="", + case_sensitive=False, + extra="allow", + ) - def __repr__(self): - return self.__str__() - def __delitem__(self, key): - del self.instance._conf[key] +class DisplaySettings(BaseSettings): + """Display settings""" - def __iter__(self): - return iter(self.instance._conf) + limit: int = 12 + width: int = 14 + show_tuple_count: bool = True - def __len__(self): - return len(self.instance._conf) + model_config = SettingsConfigDict( + env_prefix="", + case_sensitive=False, + extra="allow", + ) + + +class ExternalSettings(BaseSettings): + """External storage settings""" + + aws_access_key_id: Optional[str] = None + aws_secret_access_key: Optional[str] = None + + model_config = SettingsConfigDict( + env_prefix="DJ_", + case_sensitive=False, + extra="allow", + ) + + +class DataJointSettings(BaseSettings): + """Main DataJoint Settings using Pydantic""" + + database: DatabaseSettings = Field(default_factory=DatabaseSettings) + connection: ConnectionSettings = Field(default_factory=ConnectionSettings) + display: DisplaySettings = Field(default_factory=DisplaySettings) + external: ExternalSettings = Field(default_factory=ExternalSettings) + + loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" + safemode: bool = True + fetch_format: Literal["array", "frame"] = "array" + enable_python_native_blobs: bool = True + add_hidden_timestamp: bool = False + filepath_checksum_size_limit: Optional[int] = None + + # External stores configuration (not managed by pydantic directly) + stores: Dict[str, Dict[str, Any]] = Field(default_factory=dict) + cache: Optional[str] = None + query_cache: Optional[str] = None + + model_config = SettingsConfigDict( + env_prefix="DJ_", + case_sensitive=False, + extra="allow", + validate_assignment=True, + ) + + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + """Use custom environment settings source to avoid conflicts""" + return ( + init_settings, + EnvSettingsSource(settings_cls), + dotenv_settings, + file_secret_settings, + ) + + @field_validator("cache", "query_cache", mode="before") + @classmethod + def validate_path(cls, v: Any) -> Optional[str]: + """Convert path-like objects to strings""" + if v is None: + return v + # Convert Path-like objects to strings + if hasattr(v, "__fspath__"): + return str(v) + return v + + @field_validator("loglevel") + @classmethod + def validate_loglevel(cls, v: str) -> str: + """Validate and set logging level""" + valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} + if v not in valid_levels: + raise ValueError(f"'{v}' is not a valid logging value {tuple(valid_levels)}") + # Set the logger level + logger.setLevel(v) + return v + + +class ConfigWrapper(collections.abc.MutableMapping): + """ + Wrapper class that provides backward compatibility with the old Config interface. + Wraps a pydantic Settings instance to provide dict-like access with dot notation support. + """ + + def __init__(self, settings: DataJointSettings): + self._settings = settings + self._original_values: Dict[str, Any] = {} # For context manager support + self._extra: Dict[str, Any] = {} # Store arbitrary extra keys not in pydantic model + + @property + def _conf(self) -> Dict[str, Any]: + """Backward compatibility: expose internal config as _conf""" + result = self._to_dict() + result.update(self._extra) + return result + + def _get_nested(self, key: str) -> Any: + """Get a value using dot notation (e.g., 'database.host')""" + # Check if it's in the extra dict first + if key in self._extra: + return self._extra[key] + + parts = key.split(".") + obj = self._settings + + for part in parts: + if hasattr(obj, part): + obj = getattr(obj, part) + elif isinstance(obj, dict): + obj = obj[part] + else: + raise KeyError(f"Key '{key}' not found") + return obj + + def _set_nested(self, key: str, value: Any) -> None: + """Set a value using dot notation (e.g., 'database.host')""" + # Apply validators if they exist + if key in validators and not validators[key](value): + raise DataJointError(f"Validator for {key} did not pass") + + parts = key.split(".") + + if len(parts) == 1: + # Top-level attribute + if hasattr(self._settings, key): + setattr(self._settings, key, value) + else: + # Store in extra dict for arbitrary keys + self._extra[key] = value + else: + # Try to set in pydantic model first + try: + obj = self._settings + for part in parts[:-1]: + if hasattr(obj, part): + obj = getattr(obj, part) + elif isinstance(obj, dict): + if part not in obj: + obj[part] = {} + obj = obj[part] + else: + # Can't navigate, store as arbitrary key + self._extra[key] = value + return + + # Set the final value + final_key = parts[-1] + if hasattr(obj, final_key): + setattr(obj, final_key, value) + elif isinstance(obj, dict): + obj[final_key] = value + else: + # Store as arbitrary key + self._extra[key] = value + except (AttributeError, KeyError): + # If we can't set it in the model, store as arbitrary key + self._extra[key] = value + + # Special handling for loglevel + if key == "loglevel": + logger.setLevel(value) + + def __getitem__(self, key: str) -> Any: + """Get item using dict notation""" + try: + return self._get_nested(key) + except (AttributeError, KeyError): + raise KeyError(f"Key '{key}' not found") + + def __setitem__(self, key: str, value: Any) -> None: + """Set item using dict notation""" + logger.debug(f"Setting {key} to {value}") + self._set_nested(key, value) + + def __delitem__(self, key: str) -> None: + """Delete item by setting it to None (pydantic fields) or removing it (_extra dict)""" + # Check if it's in extra dict first + if key in self._extra: + del self._extra[key] + return + + parts = key.split(".") + if len(parts) == 1: + # For pydantic model fields, set to None instead of deleting + # (deleting would break iteration over model_fields) + if key in self._settings.__class__.model_fields: + setattr(self._settings, key, None) + else: + raise KeyError(f"Key '{key}' not found") + else: + # For nested fields, also set to None + obj = self._settings + for part in parts[:-1]: + obj = getattr(obj, part) + field_name = parts[-1] + if field_name in obj.__class__.model_fields: + setattr(obj, field_name, None) + else: + raise KeyError(f"Key '{key}' not found") + + def __iter__(self) -> Iterator[str]: + """Iterate over all configuration keys (flattened)""" + return iter(self._get_all_keys()) + + def __len__(self) -> int: + """Return number of configuration keys""" + return len(self._get_all_keys()) + + def _get_all_keys(self) -> List[str]: + """Get all configuration keys in flat dot notation""" + keys: List[str] = [] + + def _extract_keys(obj: Any, prefix: str = "") -> None: + if isinstance(obj, BaseSettings): + for field_name in obj.__class__.model_fields: + field_value = getattr(obj, field_name) + full_key = f"{prefix}.{field_name}" if prefix else field_name + if isinstance(field_value, BaseSettings): + _extract_keys(field_value, full_key) + else: + keys.append(full_key) + elif isinstance(obj, dict): + for k, v in obj.items(): + full_key = f"{prefix}.{k}" if prefix else k + if isinstance(v, (dict, BaseSettings)): + _extract_keys(v, full_key) + else: + keys.append(full_key) + + _extract_keys(self._settings) + # Add extra keys + keys.extend(self._extra.keys()) + return keys + + def __str__(self) -> str: + """String representation""" + return pprint.pformat(self._to_dict(), indent=4) + + def __repr__(self) -> str: + """Repr representation""" + return self.__str__() - def save(self, filename, verbose=False): + def _to_dict(self) -> Dict[str, Any]: + """Convert settings to a flat dict with dot notation keys""" + result: Dict[str, Any] = {} + + def _flatten(obj: Any, prefix: str = "") -> None: + if isinstance(obj, BaseSettings): + for field_name in obj.__class__.model_fields: + field_value = getattr(obj, field_name) + full_key = f"{prefix}.{field_name}" if prefix else field_name + if isinstance(field_value, BaseSettings): + _flatten(field_value, full_key) + elif isinstance(field_value, dict): + result[full_key] = field_value + else: + result[full_key] = field_value + elif isinstance(obj, dict): + for k, v in obj.items(): + full_key = f"{prefix}.{k}" if prefix else k + result[full_key] = v + + _flatten(self._settings) + return result + + def __eq__(self, other: Any) -> bool: + """Compare two Config instances""" + if isinstance(other, ConfigWrapper): + return self._to_dict() == other._to_dict() + elif isinstance(other, dict): + return self._to_dict() == other + return False + + def save(self, filename: str, verbose: bool = False) -> None: """ Saves the settings in JSON format to the given file path. @@ -106,45 +424,51 @@ def save(self, filename, verbose=False): :param verbose: report having saved the settings file """ with open(filename, "w") as fid: - json.dump(self._conf, fid, indent=4) + json.dump(self._to_dict(), fid, indent=4) if verbose: logger.info("Saved settings in " + filename) - def load(self, filename): + def load(self, filename: str) -> None: """ Updates the setting from config file in JSON format. - :param filename: filename of the local JSON settings file. If None, the local config file is used. + :param filename: filename of the local JSON settings file. """ if filename is None: filename = LOCALCONFIG + with open(filename, "r") as fid: logger.info(f"DataJoint is configured from {os.path.abspath(filename)}") - self._conf.update(json.load(fid)) + data = json.load(fid) - def save_local(self, verbose=False): - """ - saves the settings in the local config file - """ + # Update settings from loaded data + for key, value in data.items(): + try: + self[key] = value + except Exception as e: + logger.warning(f"Could not set config key '{key}': {e}") + + def save_local(self, verbose: bool = False) -> None: + """saves the settings in the local config file""" self.save(LOCALCONFIG, verbose) - def save_global(self, verbose=False): - """ - saves the settings in the global config file - """ + def save_global(self, verbose: bool = False) -> None: + """saves the settings in the global config file""" self.save(os.path.expanduser(os.path.join("~", GLOBALCONFIG)), verbose) - def get_store_spec(self, store): + def get_store_spec(self, store: str) -> Dict[str, Any]: """ find configuration of external stores for blobs and attachments """ try: - spec = self["stores"][store] + spec = self._settings.stores[store] except KeyError: - raise DataJointError("Storage {store} is requested but not configured".format(store=store)) + raise DataJointError(f"Storage {store} is requested but not configured") + spec: Dict[str, Any] = dict(spec) # Make a copy spec["subfolding"] = spec.get("subfolding", DEFAULT_SUBFOLDING) - spec_keys = { # REQUIRED in uppercase and allowed in lowercase + + spec_keys_by_protocol: Dict[str, Tuple[str, ...]] = { # REQUIRED in uppercase and allowed in lowercase "file": ("PROTOCOL", "LOCATION", "subfolding", "stage"), "s3": ( "PROTOCOL", @@ -161,17 +485,14 @@ def get_store_spec(self, store): } try: - spec_keys = spec_keys[spec.get("protocol", "").lower()] + spec_keys: Tuple[str, ...] = spec_keys_by_protocol[spec.get("protocol", "").lower()] except KeyError: - raise DataJointError('Missing or invalid protocol in dj.config["stores"]["{store}"]'.format(store=store)) + raise DataJointError(f'Missing or invalid protocol in dj.config["stores"]["{store}"]') # check that all required keys are present in spec try: raise DataJointError( - 'dj.config["stores"]["{store}"] is missing "{k}"'.format( - store=store, - k=next(k.lower() for k in spec_keys if k.isupper() and k.lower() not in spec), - ) + f'dj.config["stores"]["{store}"] is missing "{next(k.lower() for k in spec_keys if k.isupper() and k.lower() not in spec)}"' # noqa: E501 ) except StopIteration: pass @@ -179,10 +500,7 @@ def get_store_spec(self, store): # check that only allowed keys are present in spec try: raise DataJointError( - 'Invalid key "{k}" in dj.config["stores"]["{store}"]'.format( - store=store, - k=next(k for k in spec if k.upper() not in spec_keys and k.lower() not in spec_keys), - ) + f'Invalid key "{next(k for k in spec if k.upper() not in spec_keys and k.lower() not in spec_keys)}" in dj.config["stores"]["{store}"]' # noqa: E501 ) except StopIteration: pass # no invalid keys @@ -190,7 +508,7 @@ def get_store_spec(self, store): return spec @contextmanager - def __call__(self, **kwargs): + def __call__(self, **kwargs: Any) -> Iterator["ConfigWrapper"]: """ The config object can also be used in a with statement to change the state of the configuration temporarily. kwargs to the context manager are the keys into config, where '.' is replaced by a @@ -201,95 +519,153 @@ def __call__(self, **kwargs): >>> with dj.config(safemode=False, database__host="localhost") as cfg: >>> # do dangerous stuff here """ + # Save current values + backup_values: Dict[str, Any] = {} + converted_kwargs: Dict[str, Any] = {k.replace("__", "."): v for k, v in kwargs.items()} try: - backup = self.instance - self.instance = Config.__Config(self.instance._conf) - new = {k.replace("__", "."): v for k, v in kwargs.items()} - self.instance._conf.update(new) - yield self - except: - self.instance = backup - raise - else: - self.instance = backup + # Save original values + for key in converted_kwargs: + try: + backup_values[key] = self[key] + except KeyError: + backup_values[key] = None - class __Config: - """ - Stores datajoint settings. Behaves like a dictionary, but applies validator functions - when certain keys are set. + # Apply new values + for key, value in converted_kwargs.items(): + self[key] = value - The default parameters are stored in datajoint.settings.default . If a local config file - exists, the settings specified in this file override the default settings. - """ + yield self - def __init__(self, *args, **kwargs): - self._conf = dict(default) - # use the free update to set keys - self._conf.update(dict(*args, **kwargs)) + finally: + # Restore original values + for key, value in backup_values.items(): + if value is not None: + self[key] = value + + +# Default configuration dictionary for backward compatibility +default = { + "database.host": "localhost", + "database.password": None, + "database.user": None, + "database.port": 3306, + "database.reconnect": True, + "connection.init_function": None, + "connection.charset": "", + "loglevel": "INFO", + "safemode": True, + "fetch_format": "array", + "display.limit": 12, + "display.width": 14, + "display.show_tuple_count": True, + "database.use_tls": None, + "enable_python_native_blobs": True, + "add_hidden_timestamp": False, + "filepath_checksum_size_limit": None, +} - def __getitem__(self, key): - return self._conf[key] +# Validators for backward compatibility +validators = collections.defaultdict(lambda: lambda value: True) +validators["database.port"] = lambda a: isinstance(a, int) - def __setitem__(self, key, value): - logger.debug("Setting {0:s} to {1:s}".format(str(key), str(value))) - if validators[key](value): - self._conf[key] = value - else: - raise DataJointError("Validator for {0:s} did not pass".format(key)) - valid_logging_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} - if key == "loglevel": - if value not in valid_logging_levels: - raise ValueError(f"'{value}' is not a valid logging value {tuple(valid_logging_levels)}") - logger.setLevel(value) +# Create settings instance +_settings = DataJointSettings() + +# Create config wrapper for backward compatibility +config = ConfigWrapper(_settings) # Load configuration from file -config = Config() config_files = (os.path.expanduser(n) for n in (LOCALCONFIG, os.path.join("~", GLOBALCONFIG))) try: config.load(next(n for n in config_files if os.path.exists(n))) except StopIteration: logger.info("No config file was found.") -# override login credentials with environment variables -mapping = { - k: v - for k, v in zip( - ( - "database.host", - "database.user", - "database.password", - "database.port", - "external.aws_access_key_id", - "external.aws_secret_access_key", - "loglevel", - ), - map( - os.getenv, - ( - "DJ_HOST", - "DJ_USER", - "DJ_PASS", - "DJ_PORT", - "DJ_AWS_ACCESS_KEY_ID", - "DJ_AWS_SECRET_ACCESS_KEY", - "DJ_LOG_LEVEL", - ), - ), - ) - if v is not None -} +# Override login credentials with environment variables +# Note: pydantic-settings already handles this through validation_alias, +# but we keep this for any custom env vars not directly mapped +mapping = {} + +# Check for any environment variables that weren't caught by pydantic +for env_key, config_key in [ + ("DJ_HOST", "database.host"), + ("DJ_USER", "database.user"), + ("DJ_PASS", "database.password"), + ("DJ_PORT", "database.port"), + ("DJ_AWS_ACCESS_KEY_ID", "external.aws_access_key_id"), + ("DJ_AWS_SECRET_ACCESS_KEY", "external.aws_secret_access_key"), + ("DJ_LOG_LEVEL", "loglevel"), +]: + env_value = os.getenv(env_key) + if env_value is not None: + # Only add to mapping if pydantic didn't already set it + try: + current_value = config[config_key] + if current_value is None or (config_key == "database.port" and current_value == 3306): + if config_key == "database.port": + try: + mapping[config_key] = int(env_value) + except ValueError: + logger.warning(f"Invalid DJ_PORT value: {env_value}, using default port 3306") + else: + mapping[config_key] = env_value + except KeyError: + # Key doesn't exist yet, add it + if config_key == "database.port": + try: + mapping[config_key] = int(env_value) + except ValueError: + logger.warning(f"Invalid DJ_PORT value: {env_value}, using default port 3306") + else: + mapping[config_key] = env_value -# Convert DJ_PORT from string to int if present -if "database.port" in mapping and mapping["database.port"] is not None: - try: - mapping["database.port"] = int(mapping["database.port"]) - except ValueError: - logger.warning(f"Invalid DJ_PORT value: {mapping['database.port']}, using default port 3306") - del mapping["database.port"] if mapping: - logger.info(f"Overloaded settings {tuple(mapping)} from environment variables.") - config.update(mapping) + logger.info(f"Overloaded settings {tuple(mapping.keys())} from environment variables.") + for key, value in mapping.items(): + config[key] = value +# Set logging level logger.setLevel(log_levels[config["loglevel"]]) + + +# Maintain singleton behavior for compatibility +class Config: + """ + Backward compatibility class that mimics the old Config singleton behavior. + This redirects all access to the global config instance. + """ + + instance = None + + def __init__(self, *args: Any, **kwargs: Any) -> None: + # Always use the global config instance + pass + + def __getattr__(self, name: str) -> Any: + return getattr(config, name) + + def __getitem__(self, item: str) -> Any: + return config[item] + + def __setitem__(self, item: str, value: Any) -> None: + config[item] = value + + def __str__(self) -> str: + return str(config) + + def __repr__(self) -> str: + return repr(config) + + def __delitem__(self, key: str) -> None: + del config[key] + + def __iter__(self) -> Iterator[str]: + return iter(config) + + def __len__(self) -> int: + return len(config) + + def __eq__(self, other: Any) -> bool: + return config.__eq__(other) From 80d6d6f0815b8e18641c9430d24b5a1602453ecf Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 17 Dec 2025 20:40:38 +0100 Subject: [PATCH 029/219] remove unnecessary class --- src/datajoint/settings.py | 79 +++------------------------------------ 1 file changed, 6 insertions(+), 73 deletions(-) diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index a7f07659b..41b5914ea 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -12,64 +12,11 @@ from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple from pydantic import Field, field_validator -from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict +from pydantic.aliases import AliasChoices +from pydantic_settings import BaseSettings, SettingsConfigDict from .errors import DataJointError - -class EnvSettingsSource(PydanticBaseSettingsSource): - """ - Custom settings source that reads legacy DJ_* environment variables. - - This is needed because DataJoint's historical environment variable names - don't follow pydantic's nested naming convention: - - DJ_HOST instead of DJ_DATABASE__HOST - - DJ_USER instead of DJ_DATABASE__USER - - DJ_PASS instead of DJ_DATABASE__PASSWORD - - This maintains backward compatibility with existing deployments. - """ - - def get_field_value(self, field: Any, field_name: str) -> Tuple[Any, str, bool]: - """Required abstract method - not used as we override __call__""" - return None, field_name, False - - def __call__(self) -> Dict[str, Any]: - d: Dict[str, Any] = {} - - # Read only DJ_* environment variables - env_mapping = { - "DJ_HOST": ("database", "host"), - "DJ_USER": ("database", "user"), - "DJ_PASS": ("database", "password"), - "DJ_PORT": ("database", "port"), - "DJ_AWS_ACCESS_KEY_ID": ("external", "aws_access_key_id"), - "DJ_AWS_SECRET_ACCESS_KEY": ("external", "aws_secret_access_key"), - "DJ_LOG_LEVEL": ("loglevel",), - } - - for env_var, path in env_mapping.items(): - env_value = os.getenv(env_var) - if env_value is not None: - if len(path) == 1: - # Top-level field - d[path[0]] = env_value - else: - # Nested field - if path[0] not in d: - d[path[0]] = {} - if path[0] == "database" and path[1] == "port": - # Convert port to integer - try: - d[path[0]][path[1]] = int(env_value) - except ValueError: - logger.warning(f"Invalid DJ_PORT value: {env_value}, using default") - else: - d[path[0]][path[1]] = env_value - - return d - - LOCALCONFIG = "dj_local_conf.json" GLOBALCONFIG = ".datajoint_config.json" # subfolding for external storage in filesystem. @@ -162,7 +109,10 @@ class DataJointSettings(BaseSettings): display: DisplaySettings = Field(default_factory=DisplaySettings) external: ExternalSettings = Field(default_factory=ExternalSettings) - loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" + loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( + default="INFO", + validation_alias=AliasChoices("loglevel", "DJ_LOG_LEVEL"), + ) safemode: bool = True fetch_format: Literal["array", "frame"] = "array" enable_python_native_blobs: bool = True @@ -181,23 +131,6 @@ class DataJointSettings(BaseSettings): validate_assignment=True, ) - @classmethod - def settings_customise_sources( - cls, - settings_cls: type[BaseSettings], - init_settings: PydanticBaseSettingsSource, - env_settings: PydanticBaseSettingsSource, - dotenv_settings: PydanticBaseSettingsSource, - file_secret_settings: PydanticBaseSettingsSource, - ) -> tuple[PydanticBaseSettingsSource, ...]: - """Use custom environment settings source to avoid conflicts""" - return ( - init_settings, - EnvSettingsSource(settings_cls), - dotenv_settings, - file_secret_settings, - ) - @field_validator("cache", "query_cache", mode="before") @classmethod def validate_path(cls, v: Any) -> Optional[str]: From 6eb7eed27d0343f2662b68e00479b396d140a736 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Dec 2025 23:18:05 +0000 Subject: [PATCH 030/219] refactor: simplify settings to pure pydantic without backward compat - Remove ConfigWrapper class and backward compatibility layer - Use direct pydantic BaseSettings with typed nested models - Change context manager from config(...) to config.override(...) - Add validate_assignment=True for runtime type checking - Improve store spec validation with clear error messages - Update all tests to use new API - Preserve dict-style access via __getitem__/__setitem__ for convenience --- src/datajoint/jobs.py | 6 +- src/datajoint/settings.py | 776 ++++++++++++------------------- tests/test_fetch.py | 8 +- tests/test_jobs.py | 2 +- tests/test_nan.py | 2 +- tests/test_relational_operand.py | 2 +- tests/test_settings.py | 325 ++++++++++--- 7 files changed, 572 insertions(+), 549 deletions(-) diff --git a/src/datajoint/jobs.py b/src/datajoint/jobs.py index dc568f256..ff6440495 100644 --- a/src/datajoint/jobs.py +++ b/src/datajoint/jobs.py @@ -76,7 +76,7 @@ def reserve(self, table_name, key): user=self._user, ) try: - with config(enable_python_native_blobs=True): + with config.override(enable_python_native_blobs=True): self.insert1(job, ignore_extra_fields=True) except DuplicateError: return False @@ -107,7 +107,7 @@ def ignore(self, table_name, key): user=self._user, ) try: - with config(enable_python_native_blobs=True): + with config.override(enable_python_native_blobs=True): self.insert1(job, ignore_extra_fields=True) except DuplicateError: return False @@ -135,7 +135,7 @@ def error(self, table_name, key, error_message, error_stack=None): """ if len(error_message) > ERROR_MESSAGE_LENGTH: error_message = error_message[: ERROR_MESSAGE_LENGTH - len(TRUNCATION_APPENDIX)] + TRUNCATION_APPENDIX - with config(enable_python_native_blobs=True): + with config.override(enable_python_native_blobs=True): self.insert1( dict( table_name=table_name, diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 41b5914ea..01b48cbfb 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -1,26 +1,36 @@ """ -Settings for DataJoint using pydantic-settings +DataJoint Settings using pydantic-settings. + +This module provides a strongly-typed configuration system for DataJoint. +Settings can be configured via: +1. Environment variables (prefixed with DJ_) +2. Configuration files (dj_local_conf.json or ~/.datajoint_config.json) +3. Direct attribute assignment + +Example usage: + >>> import datajoint as dj + >>> dj.config.database.host = "localhost" + >>> dj.config.database.port = 3306 + >>> with dj.config.override(safemode=False): + ... # dangerous operations here """ -import collections import json import logging import os -import pprint from contextlib import contextmanager +from copy import deepcopy from enum import Enum -from typing import Any, Dict, Iterator, List, Literal, Optional, Tuple +from pathlib import Path +from typing import Any, Dict, Iterator, Literal, Optional, Tuple, Union -from pydantic import Field, field_validator -from pydantic.aliases import AliasChoices +from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from .errors import DataJointError LOCALCONFIG = "dj_local_conf.json" GLOBALCONFIG = ".datajoint_config.json" -# subfolding for external storage in filesystem. -# 2, 2 means that file abcdef is stored as /ab/cd/abcdef DEFAULT_SUBFOLDING = (2, 2) Role = Enum("Role", "manual lookup imported computed job") @@ -34,84 +44,121 @@ prefix_to_role = dict(zip(role_to_prefix.values(), role_to_prefix)) logger = logging.getLogger(__name__.split(".")[0]) -log_levels = { - "INFO": logging.INFO, - "WARNING": logging.WARNING, - "CRITICAL": logging.CRITICAL, - "DEBUG": logging.DEBUG, - "ERROR": logging.ERROR, - None: logging.NOTSET, -} class DatabaseSettings(BaseSettings): - """Database connection settings""" - - host: str = "localhost" - password: Optional[str] = None - user: Optional[str] = None - port: int = 3306 - reconnect: bool = True - use_tls: Optional[bool] = None + """Database connection settings.""" model_config = SettingsConfigDict( env_prefix="DJ_", case_sensitive=False, - extra="allow", + extra="forbid", + validate_assignment=True, ) + host: str = Field(default="localhost", validation_alias="DJ_HOST") + user: Optional[str] = Field(default=None, validation_alias="DJ_USER") + password: Optional[str] = Field(default=None, validation_alias="DJ_PASS") + port: int = Field(default=3306, validation_alias="DJ_PORT") + reconnect: bool = True + use_tls: Optional[bool] = None + class ConnectionSettings(BaseSettings): - """Connection settings""" + """Connection behavior settings.""" + + model_config = SettingsConfigDict(extra="forbid", validate_assignment=True) init_function: Optional[str] = None charset: str = "" # pymysql uses '' as default - model_config = SettingsConfigDict( - env_prefix="", - case_sensitive=False, - extra="allow", - ) - class DisplaySettings(BaseSettings): - """Display settings""" + """Display and preview settings.""" + + model_config = SettingsConfigDict(extra="forbid", validate_assignment=True) limit: int = 12 width: int = 14 show_tuple_count: bool = True + +class ExternalSettings(BaseSettings): + """External storage credentials.""" + model_config = SettingsConfigDict( - env_prefix="", + env_prefix="DJ_", case_sensitive=False, - extra="allow", + extra="forbid", + validate_assignment=True, ) + aws_access_key_id: Optional[str] = Field( + default=None, validation_alias="DJ_AWS_ACCESS_KEY_ID" + ) + aws_secret_access_key: Optional[str] = Field( + default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY" + ) -class ExternalSettings(BaseSettings): - """External storage settings""" - aws_access_key_id: Optional[str] = None - aws_secret_access_key: Optional[str] = None +class StoreSpec(BaseSettings): + """Configuration for an external store.""" + + model_config = SettingsConfigDict(extra="forbid") + + protocol: Literal["file", "s3"] + location: str + subfolding: Tuple[int, ...] = DEFAULT_SUBFOLDING + stage: Optional[str] = None + + # S3-specific fields + endpoint: Optional[str] = None + bucket: Optional[str] = None + access_key: Optional[str] = None + secret_key: Optional[str] = None + secure: bool = True + proxy_server: Optional[str] = None + + @model_validator(mode="after") + def validate_s3_fields(self) -> "StoreSpec": + """Ensure S3-specific fields are provided for S3 protocol.""" + if self.protocol == "s3": + required = ["endpoint", "bucket", "access_key", "secret_key"] + missing = [f for f in required if getattr(self, f) is None] + if missing: + raise ValueError(f"S3 store requires: {', '.join(missing)}") + return self + + +class Config(BaseSettings): + """ + Main DataJoint configuration. + + Access settings via attributes: + >>> config.database.host + >>> config.safemode + + Override temporarily with context manager: + >>> with config.override(safemode=False): + ... pass + """ model_config = SettingsConfigDict( env_prefix="DJ_", case_sensitive=False, - extra="allow", + extra="forbid", + validate_assignment=True, ) - -class DataJointSettings(BaseSettings): - """Main DataJoint Settings using Pydantic""" - + # Nested settings groups database: DatabaseSettings = Field(default_factory=DatabaseSettings) connection: ConnectionSettings = Field(default_factory=ConnectionSettings) display: DisplaySettings = Field(default_factory=DisplaySettings) external: ExternalSettings = Field(default_factory=ExternalSettings) + # Top-level settings loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( - default="INFO", - validation_alias=AliasChoices("loglevel", "DJ_LOG_LEVEL"), + default="INFO", validation_alias="DJ_LOG_LEVEL" ) safemode: bool = True fetch_format: Literal["array", "frame"] = "array" @@ -119,486 +166,279 @@ class DataJointSettings(BaseSettings): add_hidden_timestamp: bool = False filepath_checksum_size_limit: Optional[int] = None - # External stores configuration (not managed by pydantic directly) + # External stores configuration stores: Dict[str, Dict[str, Any]] = Field(default_factory=dict) - cache: Optional[str] = None - query_cache: Optional[str] = None - model_config = SettingsConfigDict( - env_prefix="DJ_", - case_sensitive=False, - extra="allow", - validate_assignment=True, - ) - - @field_validator("cache", "query_cache", mode="before") - @classmethod - def validate_path(cls, v: Any) -> Optional[str]: - """Convert path-like objects to strings""" - if v is None: - return v - # Convert Path-like objects to strings - if hasattr(v, "__fspath__"): - return str(v) - return v + # Cache paths + cache: Optional[Path] = None + query_cache: Optional[Path] = None - @field_validator("loglevel") + @field_validator("loglevel", mode="after") @classmethod - def validate_loglevel(cls, v: str) -> str: - """Validate and set logging level""" - valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} - if v not in valid_levels: - raise ValueError(f"'{v}' is not a valid logging value {tuple(valid_levels)}") - # Set the logger level + def set_logger_level(cls, v: str) -> str: + """Update logger level when loglevel changes.""" logger.setLevel(v) return v + @field_validator("cache", "query_cache", mode="before") + @classmethod + def convert_path(cls, v: Any) -> Optional[Path]: + """Convert string paths to Path objects.""" + if v is None: + return None + return Path(v) if not isinstance(v, Path) else v -class ConfigWrapper(collections.abc.MutableMapping): - """ - Wrapper class that provides backward compatibility with the old Config interface. - Wraps a pydantic Settings instance to provide dict-like access with dot notation support. - """ + def get_store_spec(self, store: str) -> Dict[str, Any]: + """ + Get configuration for an external store. - def __init__(self, settings: DataJointSettings): - self._settings = settings - self._original_values: Dict[str, Any] = {} # For context manager support - self._extra: Dict[str, Any] = {} # Store arbitrary extra keys not in pydantic model + Args: + store: Name of the store to retrieve - @property - def _conf(self) -> Dict[str, Any]: - """Backward compatibility: expose internal config as _conf""" - result = self._to_dict() - result.update(self._extra) - return result + Returns: + Store configuration dict with validated fields - def _get_nested(self, key: str) -> Any: - """Get a value using dot notation (e.g., 'database.host')""" - # Check if it's in the extra dict first - if key in self._extra: - return self._extra[key] + Raises: + DataJointError: If store is not configured or has invalid config + """ + if store not in self.stores: + raise DataJointError(f"Storage '{store}' is requested but not configured") - parts = key.split(".") - obj = self._settings + spec = dict(self.stores[store]) + spec.setdefault("subfolding", DEFAULT_SUBFOLDING) - for part in parts: - if hasattr(obj, part): - obj = getattr(obj, part) - elif isinstance(obj, dict): - obj = obj[part] - else: - raise KeyError(f"Key '{key}' not found") - return obj + # Validate protocol + protocol = spec.get("protocol", "").lower() + if protocol not in ("file", "s3"): + raise DataJointError( + f'Missing or invalid protocol in config.stores["{store}"]' + ) - def _set_nested(self, key: str, value: Any) -> None: - """Set a value using dot notation (e.g., 'database.host')""" - # Apply validators if they exist - if key in validators and not validators[key](value): - raise DataJointError(f"Validator for {key} did not pass") + # Define required and allowed keys by protocol + required_keys: Dict[str, Tuple[str, ...]] = { + "file": ("protocol", "location"), + "s3": ("protocol", "endpoint", "bucket", "access_key", "secret_key", "location"), + } + allowed_keys: Dict[str, Tuple[str, ...]] = { + "file": ("protocol", "location", "subfolding", "stage"), + "s3": ( + "protocol", "endpoint", "bucket", "access_key", "secret_key", + "location", "secure", "subfolding", "stage", "proxy_server", + ), + } - parts = key.split(".") + # Check required keys + missing = [k for k in required_keys[protocol] if k not in spec] + if missing: + raise DataJointError( + f'config.stores["{store}"] is missing: {", ".join(missing)}' + ) - if len(parts) == 1: - # Top-level attribute - if hasattr(self._settings, key): - setattr(self._settings, key, value) - else: - # Store in extra dict for arbitrary keys - self._extra[key] = value - else: - # Try to set in pydantic model first - try: - obj = self._settings - for part in parts[:-1]: - if hasattr(obj, part): - obj = getattr(obj, part) - elif isinstance(obj, dict): - if part not in obj: - obj[part] = {} - obj = obj[part] - else: - # Can't navigate, store as arbitrary key - self._extra[key] = value - return - - # Set the final value - final_key = parts[-1] - if hasattr(obj, final_key): - setattr(obj, final_key, value) - elif isinstance(obj, dict): - obj[final_key] = value - else: - # Store as arbitrary key - self._extra[key] = value - except (AttributeError, KeyError): - # If we can't set it in the model, store as arbitrary key - self._extra[key] = value - - # Special handling for loglevel - if key == "loglevel": - logger.setLevel(value) + # Check for invalid keys + invalid = [k for k in spec if k not in allowed_keys[protocol]] + if invalid: + raise DataJointError( + f'Invalid key(s) in config.stores["{store}"]: {", ".join(invalid)}' + ) - def __getitem__(self, key: str) -> Any: - """Get item using dict notation""" - try: - return self._get_nested(key) - except (AttributeError, KeyError): - raise KeyError(f"Key '{key}' not found") + return spec - def __setitem__(self, key: str, value: Any) -> None: - """Set item using dict notation""" - logger.debug(f"Setting {key} to {value}") - self._set_nested(key, value) + def save(self, filename: Union[str, Path], verbose: bool = False) -> None: + """ + Save settings to a JSON file. - def __delitem__(self, key: str) -> None: - """Delete item by setting it to None (pydantic fields) or removing it (_extra dict)""" - # Check if it's in extra dict first - if key in self._extra: - del self._extra[key] - return + Args: + filename: Path to save the configuration + verbose: If True, log the save operation + """ + data = self._to_flat_dict() + with open(filename, "w") as f: + json.dump(data, f, indent=4, default=str) + if verbose: + logger.info(f"Saved settings to {filename}") - parts = key.split(".") - if len(parts) == 1: - # For pydantic model fields, set to None instead of deleting - # (deleting would break iteration over model_fields) - if key in self._settings.__class__.model_fields: - setattr(self._settings, key, None) - else: - raise KeyError(f"Key '{key}' not found") - else: - # For nested fields, also set to None - obj = self._settings - for part in parts[:-1]: - obj = getattr(obj, part) - field_name = parts[-1] - if field_name in obj.__class__.model_fields: - setattr(obj, field_name, None) - else: - raise KeyError(f"Key '{key}' not found") + def save_local(self, verbose: bool = False) -> None: + """Save settings to local config file (dj_local_conf.json).""" + self.save(LOCALCONFIG, verbose) - def __iter__(self) -> Iterator[str]: - """Iterate over all configuration keys (flattened)""" - return iter(self._get_all_keys()) + def save_global(self, verbose: bool = False) -> None: + """Save settings to global config file (~/.datajoint_config.json).""" + self.save(Path.home() / GLOBALCONFIG, verbose) - def __len__(self) -> int: - """Return number of configuration keys""" - return len(self._get_all_keys()) + def load(self, filename: Union[str, Path, None] = None) -> None: + """ + Load settings from a JSON file. - def _get_all_keys(self) -> List[str]: - """Get all configuration keys in flat dot notation""" - keys: List[str] = [] + Args: + filename: Path to load configuration from. If None, uses LOCALCONFIG. + """ + if filename is None: + filename = LOCALCONFIG - def _extract_keys(obj: Any, prefix: str = "") -> None: - if isinstance(obj, BaseSettings): - for field_name in obj.__class__.model_fields: - field_value = getattr(obj, field_name) - full_key = f"{prefix}.{field_name}" if prefix else field_name - if isinstance(field_value, BaseSettings): - _extract_keys(field_value, full_key) - else: - keys.append(full_key) - elif isinstance(obj, dict): - for k, v in obj.items(): - full_key = f"{prefix}.{k}" if prefix else k - if isinstance(v, (dict, BaseSettings)): - _extract_keys(v, full_key) - else: - keys.append(full_key) + filepath = Path(filename) + if not filepath.exists(): + raise FileNotFoundError(f"Config file not found: {filepath}") - _extract_keys(self._settings) - # Add extra keys - keys.extend(self._extra.keys()) - return keys + logger.info(f"Loading configuration from {filepath.absolute()}") - def __str__(self) -> str: - """String representation""" - return pprint.pformat(self._to_dict(), indent=4) + with open(filepath) as f: + data = json.load(f) - def __repr__(self) -> str: - """Repr representation""" - return self.__str__() + self._update_from_flat_dict(data) - def _to_dict(self) -> Dict[str, Any]: - """Convert settings to a flat dict with dot notation keys""" + def _to_flat_dict(self) -> Dict[str, Any]: + """Convert settings to flat dict with dot notation keys.""" result: Dict[str, Any] = {} - def _flatten(obj: Any, prefix: str = "") -> None: + def flatten(obj: Any, prefix: str = "") -> None: if isinstance(obj, BaseSettings): - for field_name in obj.__class__.model_fields: - field_value = getattr(obj, field_name) - full_key = f"{prefix}.{field_name}" if prefix else field_name - if isinstance(field_value, BaseSettings): - _flatten(field_value, full_key) - elif isinstance(field_value, dict): - result[full_key] = field_value + for name in obj.model_fields: + value = getattr(obj, name) + key = f"{prefix}.{name}" if prefix else name + if isinstance(value, BaseSettings): + flatten(value, key) + elif isinstance(value, Path): + result[key] = str(value) else: - result[full_key] = field_value + result[key] = value elif isinstance(obj, dict): - for k, v in obj.items(): - full_key = f"{prefix}.{k}" if prefix else k - result[full_key] = v + result[prefix] = obj - _flatten(self._settings) + flatten(self) return result - def __eq__(self, other: Any) -> bool: - """Compare two Config instances""" - if isinstance(other, ConfigWrapper): - return self._to_dict() == other._to_dict() - elif isinstance(other, dict): - return self._to_dict() == other - return False - - def save(self, filename: str, verbose: bool = False) -> None: - """ - Saves the settings in JSON format to the given file path. - - :param filename: filename of the local JSON settings file. - :param verbose: report having saved the settings file - """ - with open(filename, "w") as fid: - json.dump(self._to_dict(), fid, indent=4) - if verbose: - logger.info("Saved settings in " + filename) - - def load(self, filename: str) -> None: - """ - Updates the setting from config file in JSON format. - - :param filename: filename of the local JSON settings file. - """ - if filename is None: - filename = LOCALCONFIG - - with open(filename, "r") as fid: - logger.info(f"DataJoint is configured from {os.path.abspath(filename)}") - data = json.load(fid) - - # Update settings from loaded data - for key, value in data.items(): - try: - self[key] = value - except Exception as e: - logger.warning(f"Could not set config key '{key}': {e}") - - def save_local(self, verbose: bool = False) -> None: - """saves the settings in the local config file""" - self.save(LOCALCONFIG, verbose) - - def save_global(self, verbose: bool = False) -> None: - """saves the settings in the global config file""" - self.save(os.path.expanduser(os.path.join("~", GLOBALCONFIG)), verbose) + def _update_from_flat_dict(self, data: Dict[str, Any]) -> None: + """Update settings from a flat dict with dot notation keys.""" + for key, value in data.items(): + parts = key.split(".") + if len(parts) == 1: + if hasattr(self, key): + setattr(self, key, value) + elif len(parts) == 2: + group, attr = parts + if hasattr(self, group): + group_obj = getattr(self, group) + if hasattr(group_obj, attr): + setattr(group_obj, attr, value) - def get_store_spec(self, store: str) -> Dict[str, Any]: - """ - find configuration of external stores for blobs and attachments + @contextmanager + def override(self, **kwargs: Any) -> Iterator["Config"]: """ - try: - spec = self._settings.stores[store] - except KeyError: - raise DataJointError(f"Storage {store} is requested but not configured") - - spec: Dict[str, Any] = dict(spec) # Make a copy - spec["subfolding"] = spec.get("subfolding", DEFAULT_SUBFOLDING) - - spec_keys_by_protocol: Dict[str, Tuple[str, ...]] = { # REQUIRED in uppercase and allowed in lowercase - "file": ("PROTOCOL", "LOCATION", "subfolding", "stage"), - "s3": ( - "PROTOCOL", - "ENDPOINT", - "BUCKET", - "ACCESS_KEY", - "SECRET_KEY", - "LOCATION", - "secure", - "subfolding", - "stage", - "proxy_server", - ), - } - - try: - spec_keys: Tuple[str, ...] = spec_keys_by_protocol[spec.get("protocol", "").lower()] - except KeyError: - raise DataJointError(f'Missing or invalid protocol in dj.config["stores"]["{store}"]') - - # check that all required keys are present in spec - try: - raise DataJointError( - f'dj.config["stores"]["{store}"] is missing "{next(k.lower() for k in spec_keys if k.isupper() and k.lower() not in spec)}"' # noqa: E501 - ) - except StopIteration: - pass + Temporarily override configuration values. - # check that only allowed keys are present in spec - try: - raise DataJointError( - f'Invalid key "{next(k for k in spec if k.upper() not in spec_keys and k.lower() not in spec_keys)}" in dj.config["stores"]["{store}"]' # noqa: E501 - ) - except StopIteration: - pass # no invalid keys - - return spec + Args: + **kwargs: Settings to override. Use double underscore for nested + settings (e.g., database__host="localhost") - @contextmanager - def __call__(self, **kwargs: Any) -> Iterator["ConfigWrapper"]: - """ - The config object can also be used in a with statement to change the state of the configuration - temporarily. kwargs to the context manager are the keys into config, where '.' is replaced by a - double underscore '__'. The context manager yields the changed config object. + Yields: + The config instance with overridden values Example: - >>> import datajoint as dj - >>> with dj.config(safemode=False, database__host="localhost") as cfg: - >>> # do dangerous stuff here + >>> with config.override(safemode=False, database__host="test"): + ... # config.safemode is False here + ... pass + >>> # config.safemode is restored """ - # Save current values - backup_values: Dict[str, Any] = {} - converted_kwargs: Dict[str, Any] = {k.replace("__", "."): v for k, v in kwargs.items()} + # Store original values + backup = {} + + # Convert double underscore to nested access + converted = {} + for key, value in kwargs.items(): + if "__" in key: + parts = key.split("__") + converted[tuple(parts)] = value + else: + converted[(key,)] = value try: - # Save original values - for key in converted_kwargs: - try: - backup_values[key] = self[key] - except KeyError: - backup_values[key] = None - - # Apply new values - for key, value in converted_kwargs.items(): - self[key] = value + # Save originals and apply overrides + for key_parts, value in converted.items(): + if len(key_parts) == 1: + key = key_parts[0] + if hasattr(self, key): + backup[key_parts] = deepcopy(getattr(self, key)) + setattr(self, key, value) + elif len(key_parts) == 2: + group, attr = key_parts + if hasattr(self, group): + group_obj = getattr(self, group) + if hasattr(group_obj, attr): + backup[key_parts] = deepcopy(getattr(group_obj, attr)) + setattr(group_obj, attr, value) yield self finally: # Restore original values - for key, value in backup_values.items(): - if value is not None: - self[key] = value - - -# Default configuration dictionary for backward compatibility -default = { - "database.host": "localhost", - "database.password": None, - "database.user": None, - "database.port": 3306, - "database.reconnect": True, - "connection.init_function": None, - "connection.charset": "", - "loglevel": "INFO", - "safemode": True, - "fetch_format": "array", - "display.limit": 12, - "display.width": 14, - "display.show_tuple_count": True, - "database.use_tls": None, - "enable_python_native_blobs": True, - "add_hidden_timestamp": False, - "filepath_checksum_size_limit": None, -} - -# Validators for backward compatibility -validators = collections.defaultdict(lambda: lambda value: True) -validators["database.port"] = lambda a: isinstance(a, int) - - -# Create settings instance -_settings = DataJointSettings() - -# Create config wrapper for backward compatibility -config = ConfigWrapper(_settings) - -# Load configuration from file -config_files = (os.path.expanduser(n) for n in (LOCALCONFIG, os.path.join("~", GLOBALCONFIG))) -try: - config.load(next(n for n in config_files if os.path.exists(n))) -except StopIteration: - logger.info("No config file was found.") - -# Override login credentials with environment variables -# Note: pydantic-settings already handles this through validation_alias, -# but we keep this for any custom env vars not directly mapped -mapping = {} - -# Check for any environment variables that weren't caught by pydantic -for env_key, config_key in [ - ("DJ_HOST", "database.host"), - ("DJ_USER", "database.user"), - ("DJ_PASS", "database.password"), - ("DJ_PORT", "database.port"), - ("DJ_AWS_ACCESS_KEY_ID", "external.aws_access_key_id"), - ("DJ_AWS_SECRET_ACCESS_KEY", "external.aws_secret_access_key"), - ("DJ_LOG_LEVEL", "loglevel"), -]: - env_value = os.getenv(env_key) - if env_value is not None: - # Only add to mapping if pydantic didn't already set it - try: - current_value = config[config_key] - if current_value is None or (config_key == "database.port" and current_value == 3306): - if config_key == "database.port": - try: - mapping[config_key] = int(env_value) - except ValueError: - logger.warning(f"Invalid DJ_PORT value: {env_value}, using default port 3306") - else: - mapping[config_key] = env_value - except KeyError: - # Key doesn't exist yet, add it - if config_key == "database.port": - try: - mapping[config_key] = int(env_value) - except ValueError: - logger.warning(f"Invalid DJ_PORT value: {env_value}, using default port 3306") + for key_parts, original in backup.items(): + if len(key_parts) == 1: + setattr(self, key_parts[0], original) + elif len(key_parts) == 2: + group, attr = key_parts + setattr(getattr(self, group), attr, original) + + # Backward compatibility: dict-like access + def __getitem__(self, key: str) -> Any: + """Get setting by dot-notation key (e.g., 'database.host').""" + parts = key.split(".") + obj: Any = self + for part in parts: + if hasattr(obj, part): + obj = getattr(obj, part) + elif isinstance(obj, dict): + obj = obj[part] else: - mapping[config_key] = env_value - -if mapping: - logger.info(f"Overloaded settings {tuple(mapping.keys())} from environment variables.") - for key, value in mapping.items(): - config[key] = value - -# Set logging level -logger.setLevel(log_levels[config["loglevel"]]) - - -# Maintain singleton behavior for compatibility -class Config: - """ - Backward compatibility class that mimics the old Config singleton behavior. - This redirects all access to the global config instance. - """ - - instance = None + raise KeyError(f"Setting '{key}' not found") + return obj - def __init__(self, *args: Any, **kwargs: Any) -> None: - # Always use the global config instance - pass + def __setitem__(self, key: str, value: Any) -> None: + """Set setting by dot-notation key (e.g., 'database.host').""" + parts = key.split(".") + if len(parts) == 1: + if hasattr(self, key): + setattr(self, key, value) + else: + raise KeyError(f"Setting '{key}' not found") + else: + obj: Any = self + for part in parts[:-1]: + obj = getattr(obj, part) + setattr(obj, parts[-1], value) - def __getattr__(self, name: str) -> Any: - return getattr(config, name) + def get(self, key: str, default: Any = None) -> Any: + """Get setting with optional default value.""" + try: + return self[key] + except KeyError: + return default - def __getitem__(self, item: str) -> Any: - return config[item] - def __setitem__(self, item: str, value: Any) -> None: - config[item] = value +def _create_config() -> Config: + """Create and initialize the global config instance.""" + cfg = Config() - def __str__(self) -> str: - return str(config) + # Try to load from config file + config_paths = [ + Path(LOCALCONFIG), + Path.home() / GLOBALCONFIG, + ] - def __repr__(self) -> str: - return repr(config) + for path in config_paths: + if path.exists(): + try: + cfg.load(path) + break + except Exception as e: + logger.warning(f"Failed to load config from {path}: {e}") + else: + logger.debug("No config file found, using defaults and environment variables") - def __delitem__(self, key: str) -> None: - del config[key] + # Set initial log level + logger.setLevel(cfg.loglevel) - def __iter__(self) -> Iterator[str]: - return iter(config) + return cfg - def __len__(self) -> int: - return len(config) - def __eq__(self, other: Any) -> bool: - return config.__eq__(other) +# Global config instance +config = _create_config() diff --git a/tests/test_fetch.py b/tests/test_fetch.py index 0ebe297d9..48251e195 100644 --- a/tests/test_fetch.py +++ b/tests/test_fetch.py @@ -250,7 +250,7 @@ def test_nullable_numbers(schema_any): def test_fetch_format(subject): """test fetch_format='frame'""" - with dj.config(fetch_format="frame"): + with dj.config.override(fetch_format="frame"): # test if lists are both dicts list1 = sorted(subject.proj().fetch(as_dict=True), key=itemgetter("subject_id")) list2 = sorted(subject.fetch(dj.key), key=itemgetter("subject_id")) @@ -273,9 +273,9 @@ def test_fetch_format(subject): def test_key_fetch1(subject): """test KEY fetch1 - issue #976""" - with dj.config(fetch_format="array"): + with dj.config.override(fetch_format="array"): k1 = (subject & "subject_id=10").fetch1("KEY") - with dj.config(fetch_format="frame"): + with dj.config.override(fetch_format="frame"): k2 = (subject & "subject_id=10").fetch1("KEY") assert k1 == k2 @@ -290,7 +290,7 @@ def test_query_caching(schema_any): # initialize cache directory os.mkdir(os.path.expanduser("~/dj_query_cache")) - with dj.config(query_cache=os.path.expanduser("~/dj_query_cache")): + with dj.config.override(query_cache=os.path.expanduser("~/dj_query_cache")): conn = schema.TTest3.connection # insert sample data and load cache schema.TTest3.insert([dict(key=100 + i, value=200 + i) for i in range(2)]) diff --git a/tests/test_jobs.py b/tests/test_jobs.py index 70987f716..4ffc431fe 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -83,7 +83,7 @@ def test_sigterm(clean_jobs, schema_any): def test_suppress_dj_errors(clean_jobs, schema_any): """test_suppress_dj_errors: dj errors suppressible w/o native py blobs""" - with dj.config(enable_python_native_blobs=False): + with dj.config.override(enable_python_native_blobs=False): schema.ErrorClass.populate(reserve_jobs=True, suppress_errors=True) assert len(schema.DjExceptionName()) == len(schema_any.jobs) > 0 diff --git a/tests/test_nan.py b/tests/test_nan.py index 24e6d13da..aa8db06f7 100644 --- a/tests/test_nan.py +++ b/tests/test_nan.py @@ -28,7 +28,7 @@ def arr_a(): @pytest.fixture def schema_nan_pop(schema_nan, arr_a): rel = NanTest() - with dj.config(safemode=False): + with dj.config.override(safemode=False): rel.delete() rel.insert(((i, value) for i, value in enumerate(arr_a))) return schema_nan diff --git a/tests/test_relational_operand.py b/tests/test_relational_operand.py index 371dc03e6..6a5df8331 100644 --- a/tests/test_relational_operand.py +++ b/tests/test_relational_operand.py @@ -198,7 +198,7 @@ def test_outer_union_fail_2(schema_any_pop): def test_preview(schema_simp_pop): - with dj.config(display__limit=7): + with dj.config.override(display__limit=7): x = A().proj(a="id_a") s = x.preview() assert len(s.split("\n")) == len(x) + 2 diff --git a/tests/test_settings.py b/tests/test_settings.py index 3a91719a8..ad9d968ab 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,100 +1,283 @@ +"""Tests for DataJoint settings module.""" + import os -import pprint -import random -import string +import tempfile +from pathlib import Path import pytest +from pydantic import ValidationError import datajoint as dj -from datajoint import DataJointError, settings +from datajoint import settings +from datajoint.errors import DataJointError + + +class TestSettingsAccess: + """Test accessing settings via different methods.""" + + def test_attribute_access(self): + """Test accessing settings via attributes.""" + assert dj.config.database.host == "localhost" + assert dj.config.database.port == 3306 + assert dj.config.safemode is True + + def test_dict_style_access(self): + """Test accessing settings via dict-style notation.""" + assert dj.config["database.host"] == "localhost" + assert dj.config["database.port"] == 3306 + assert dj.config["safemode"] is True + + def test_get_with_default(self): + """Test get() method with default values.""" + assert dj.config.get("database.host") == "localhost" + assert dj.config.get("nonexistent.key", "default") == "default" + assert dj.config.get("nonexistent.key") is None + + +class TestSettingsModification: + """Test modifying settings.""" + + def test_attribute_assignment(self): + """Test setting values via attribute assignment.""" + original = dj.config.database.host + try: + dj.config.database.host = "testhost" + assert dj.config.database.host == "testhost" + finally: + dj.config.database.host = original + + def test_dict_style_assignment(self): + """Test setting values via dict-style notation.""" + original = dj.config["database.host"] + try: + dj.config["database.host"] = "testhost2" + assert dj.config["database.host"] == "testhost2" + finally: + dj.config["database.host"] = original + + def test_nested_assignment(self): + """Test setting nested values.""" + original = dj.config.display.limit + try: + dj.config.display.limit = 25 + assert dj.config.display.limit == 25 + assert dj.config["display.limit"] == 25 + finally: + dj.config.display.limit = original + + +class TestTypeValidation: + """Test pydantic type validation.""" + + def test_port_must_be_integer(self): + """Test that port must be an integer.""" + with pytest.raises(ValidationError): + dj.config.database.port = "not_an_integer" + + def test_loglevel_validation(self): + """Test that loglevel must be a valid level.""" + with pytest.raises(ValidationError): + dj.config.loglevel = "INVALID_LEVEL" + + def test_fetch_format_validation(self): + """Test that fetch_format must be array or frame.""" + with pytest.raises(ValidationError): + dj.config.fetch_format = "invalid" + + def test_valid_loglevel_values(self): + """Test setting valid log levels.""" + original = dj.config.loglevel + try: + for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: + dj.config.loglevel = level + assert dj.config.loglevel == level + finally: + dj.config.loglevel = original + + +class TestContextManager: + """Test the override context manager.""" + + def test_override_simple_value(self): + """Test overriding a simple value.""" + original = dj.config.safemode + with dj.config.override(safemode=False): + assert dj.config.safemode is False + assert dj.config.safemode == original + + def test_override_nested_value(self): + """Test overriding nested values with double underscore.""" + original = dj.config.database.host + with dj.config.override(database__host="override_host"): + assert dj.config.database.host == "override_host" + assert dj.config.database.host == original + + def test_override_multiple_values(self): + """Test overriding multiple values at once.""" + orig_safe = dj.config.safemode + orig_host = dj.config.database.host + with dj.config.override(safemode=False, database__host="multi_test"): + assert dj.config.safemode is False + assert dj.config.database.host == "multi_test" + assert dj.config.safemode == orig_safe + assert dj.config.database.host == orig_host + + def test_override_restores_on_exception(self): + """Test that override restores values even when exception occurs.""" + original = dj.config.safemode + try: + with dj.config.override(safemode=False): + assert dj.config.safemode is False + raise ValueError("test exception") + except ValueError: + pass + assert dj.config.safemode == original -__author__ = "Fabian Sinz" +class TestSaveLoad: + """Test saving and loading configuration.""" -def test_load_save(): - """Testing load and save""" - dj.config.save("tmp.json") - conf = settings.Config() - conf.load("tmp.json") - assert conf == dj.config - os.remove("tmp.json") + def test_save_and_load(self): + """Test saving and loading configuration.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + filename = f.name + try: + # Modify and save + original_host = dj.config.database.host + dj.config.database.host = "saved_host" + dj.config.save(filename) -def test_singleton(): - """Testing singleton property""" - dj.config.save("tmp.json") - conf = settings.Config() - conf.load("tmp.json") - conf["dummy.val"] = 2 + # Reset and load + dj.config.database.host = "reset_host" + dj.config.load(filename) - assert conf == dj.config - os.remove("tmp.json") + assert dj.config.database.host == "saved_host" + finally: + dj.config.database.host = original_host + os.unlink(filename) + def test_save_local(self): + """Test save_local creates local config file.""" + backup_path = None + if os.path.exists(settings.LOCALCONFIG): + backup_path = settings.LOCALCONFIG + ".backup" + os.rename(settings.LOCALCONFIG, backup_path) -def test_singleton2(): - """Testing singleton property""" - conf = settings.Config() - conf["dummy.val"] = 2 - _ = settings.Config() # a new instance should not delete dummy.val - assert conf["dummy.val"] == 2 + try: + dj.config.save_local() + assert os.path.exists(settings.LOCALCONFIG) + finally: + if os.path.exists(settings.LOCALCONFIG): + os.remove(settings.LOCALCONFIG) + if backup_path and os.path.exists(backup_path): + os.rename(backup_path, settings.LOCALCONFIG) + def test_load_nonexistent_file(self): + """Test loading nonexistent file raises FileNotFoundError.""" + with pytest.raises(FileNotFoundError): + dj.config.load("/nonexistent/path/config.json") -def test_validator(): - """Testing validator""" - with pytest.raises(DataJointError): - dj.config["database.port"] = "harbor" +class TestStoreSpec: + """Test external store configuration.""" -def test_del(): - """Testing del""" - dj.config["peter"] = 2 - assert "peter" in dj.config - del dj.config["peter"] - assert "peter" not in dj.config + def test_get_store_spec_not_configured(self): + """Test getting unconfigured store raises error.""" + with pytest.raises(DataJointError, match="not configured"): + dj.config.get_store_spec("nonexistent_store") + def test_get_store_spec_file_protocol(self): + """Test file protocol store spec validation.""" + original_stores = dj.config.stores.copy() + try: + dj.config.stores["test_file"] = { + "protocol": "file", + "location": "/tmp/test", + } + spec = dj.config.get_store_spec("test_file") + assert spec["protocol"] == "file" + assert spec["location"] == "/tmp/test" + assert spec["subfolding"] == settings.DEFAULT_SUBFOLDING + finally: + dj.config.stores = original_stores -def test_len(): - """Testing len""" - len(dj.config) == len(dj.config._conf) + def test_get_store_spec_missing_required(self): + """Test missing required keys raises error.""" + original_stores = dj.config.stores.copy() + try: + dj.config.stores["bad_store"] = { + "protocol": "file", + # missing location + } + with pytest.raises(DataJointError, match="missing"): + dj.config.get_store_spec("bad_store") + finally: + dj.config.stores = original_stores + def test_get_store_spec_invalid_key(self): + """Test invalid keys in store spec raises error.""" + original_stores = dj.config.stores.copy() + try: + dj.config.stores["bad_store"] = { + "protocol": "file", + "location": "/tmp/test", + "invalid_key": "value", + } + with pytest.raises(DataJointError, match="Invalid"): + dj.config.get_store_spec("bad_store") + finally: + dj.config.stores = original_stores -def test_str(): - """Testing str""" - str(dj.config) == pprint.pformat(dj.config._conf, indent=4) +class TestDisplaySettings: + """Test display-related settings.""" -def test_repr(): - """Testing repr""" - repr(dj.config) == pprint.pformat(dj.config._conf, indent=4) + def test_display_limit(self): + """Test display limit setting.""" + original = dj.config.display.limit + try: + dj.config.display.limit = 50 + assert dj.config.display.limit == 50 + finally: + dj.config.display.limit = original + def test_display_width(self): + """Test display width setting.""" + original = dj.config.display.width + try: + dj.config.display.width = 20 + assert dj.config.display.width == 20 + finally: + dj.config.display.width = original -def test_save(): - """Testing save of config""" - tmpfile = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(20)) - moved = False - if os.path.isfile(settings.LOCALCONFIG): - os.rename(settings.LOCALCONFIG, tmpfile) - moved = True - dj.config.save_local() - assert os.path.isfile(settings.LOCALCONFIG) - if moved: - os.rename(tmpfile, settings.LOCALCONFIG) +class TestCachePaths: + """Test cache path settings.""" -def test_load_save_2(): - """Testing load and save of config""" - filename_old = dj.settings.LOCALCONFIG - filename = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(50)) + ".json" - dj.settings.LOCALCONFIG = filename - dj.config.save_local() - dj.config.load(filename=filename) - dj.settings.LOCALCONFIG = filename_old - os.remove(filename) + def test_cache_path_string(self): + """Test setting cache path as string.""" + original = dj.config.cache + try: + dj.config.cache = "/tmp/cache" + assert dj.config.cache == Path("/tmp/cache") + finally: + dj.config.cache = original + def test_cache_path_none(self): + """Test cache path can be None.""" + original = dj.config.cache + try: + dj.config.cache = None + assert dj.config.cache is None + finally: + dj.config.cache = original -def test_contextmanager(): - """Testing context manager""" - dj.config["arbitrary.stuff"] = 7 - with dj.config(arbitrary__stuff=10): - assert dj.config["arbitrary.stuff"] == 10 - assert dj.config["arbitrary.stuff"] == 7 + def test_query_cache_path(self): + """Test query cache path setting.""" + original = dj.config.query_cache + try: + dj.config.query_cache = "/tmp/query_cache" + assert dj.config.query_cache == Path("/tmp/query_cache") + finally: + dj.config.query_cache = original From f5077db783c5c7fa81313b50777f585c9416d418 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Dec 2025 23:53:09 +0000 Subject: [PATCH 031/219] feat: add recursive config search and secrets separation Config file search: - Search for datajoint.json recursively up from cwd - Stop at .git/.hg boundaries or filesystem root - Warn if no config file found (instead of silently using defaults) Secrets management: - Add .secrets/ directory support (next to datajoint.json) - Support /run/secrets/datajoint/ for Docker/Kubernetes - Use SecretStr for password and aws_secret_access_key - Secrets masked in repr/logs, excluded from save() - Dict access automatically unwraps SecretStr for compatibility Breaking changes: - Config file renamed from dj_local_conf.json to datajoint.json - No more ~/.datajoint_config.json (project-only config) - Secrets should be in env vars or .secrets/ directory --- src/datajoint/settings.py | 248 +++++++++++++++++++++++++++----------- tests/test_settings.py | 248 +++++++++++++++++++++++++------------- 2 files changed, 340 insertions(+), 156 deletions(-) diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 01b48cbfb..7b2ef3fa4 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -2,35 +2,47 @@ DataJoint Settings using pydantic-settings. This module provides a strongly-typed configuration system for DataJoint. -Settings can be configured via: -1. Environment variables (prefixed with DJ_) -2. Configuration files (dj_local_conf.json or ~/.datajoint_config.json) -3. Direct attribute assignment + +Configuration sources (in priority order): +1. Environment variables (DJ_*) +2. Secrets directories (.secrets/ in project, /run/secrets/datajoint/) +3. Project config file (datajoint.json, searched recursively up to .git/.hg) Example usage: >>> import datajoint as dj - >>> dj.config.database.host = "localhost" - >>> dj.config.database.port = 3306 + >>> dj.config.database.host + 'localhost' >>> with dj.config.override(safemode=False): ... # dangerous operations here + +Project structure: + myproject/ + ├── .git/ + ├── datajoint.json # Project config (commit this) + ├── .secrets/ # Local secrets (gitignore this) + │ ├── database.password + │ └── aws.secret_access_key + └── src/ + └── analysis.py # Config found via parent search """ import json import logging -import os +import warnings from contextlib import contextmanager from copy import deepcopy from enum import Enum from pathlib import Path from typing import Any, Dict, Iterator, Literal, Optional, Tuple, Union -from pydantic import Field, field_validator, model_validator +from pydantic import Field, SecretStr, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from .errors import DataJointError -LOCALCONFIG = "dj_local_conf.json" -GLOBALCONFIG = ".datajoint_config.json" +CONFIG_FILENAME = "datajoint.json" +SECRETS_DIRNAME = ".secrets" +SYSTEM_SECRETS_DIR = Path("/run/secrets/datajoint") DEFAULT_SUBFOLDING = (2, 2) Role = Enum("Role", "manual lookup imported computed job") @@ -46,6 +58,85 @@ logger = logging.getLogger(__name__.split(".")[0]) +def find_config_file(start: Optional[Path] = None) -> Optional[Path]: + """ + Search for datajoint.json in current and parent directories. + + Searches upward from `start` (default: cwd) until finding the config file + or hitting a project boundary (.git, .hg) or filesystem root. + + Args: + start: Directory to start search from. Defaults to current working directory. + + Returns: + Path to config file if found, None otherwise. + """ + current = (start or Path.cwd()).resolve() + + while True: + config_path = current / CONFIG_FILENAME + if config_path.is_file(): + return config_path + + # Stop at project/repo root + if (current / ".git").exists() or (current / ".hg").exists(): + return None + + # Stop at filesystem root + if current == current.parent: + return None + + current = current.parent + + +def find_secrets_dir(config_path: Optional[Path] = None) -> Optional[Path]: + """ + Find the secrets directory. + + Priority: + 1. .secrets/ in same directory as datajoint.json (project secrets) + 2. /run/secrets/datajoint/ (Docker/Kubernetes secrets) + + Args: + config_path: Path to datajoint.json if found. + + Returns: + Path to secrets directory if found, None otherwise. + """ + # Check project secrets directory (next to config file) + if config_path is not None: + project_secrets = config_path.parent / SECRETS_DIRNAME + if project_secrets.is_dir(): + return project_secrets + + # Check system secrets directory (Docker/Kubernetes) + if SYSTEM_SECRETS_DIR.is_dir(): + return SYSTEM_SECRETS_DIR + + return None + + +def read_secret_file(secrets_dir: Optional[Path], name: str) -> Optional[str]: + """ + Read a secret value from a file in the secrets directory. + + Args: + secrets_dir: Path to secrets directory. + name: Name of the secret file (e.g., 'database.password'). + + Returns: + Secret value as string, or None if not found. + """ + if secrets_dir is None: + return None + + secret_path = secrets_dir / name + if secret_path.is_file(): + return secret_path.read_text().strip() + + return None + + class DatabaseSettings(BaseSettings): """Database connection settings.""" @@ -58,7 +149,7 @@ class DatabaseSettings(BaseSettings): host: str = Field(default="localhost", validation_alias="DJ_HOST") user: Optional[str] = Field(default=None, validation_alias="DJ_USER") - password: Optional[str] = Field(default=None, validation_alias="DJ_PASS") + password: Optional[SecretStr] = Field(default=None, validation_alias="DJ_PASS") port: int = Field(default=3306, validation_alias="DJ_PORT") reconnect: bool = True use_tls: Optional[bool] = None @@ -96,44 +187,21 @@ class ExternalSettings(BaseSettings): aws_access_key_id: Optional[str] = Field( default=None, validation_alias="DJ_AWS_ACCESS_KEY_ID" ) - aws_secret_access_key: Optional[str] = Field( + aws_secret_access_key: Optional[SecretStr] = Field( default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY" ) -class StoreSpec(BaseSettings): - """Configuration for an external store.""" - - model_config = SettingsConfigDict(extra="forbid") - - protocol: Literal["file", "s3"] - location: str - subfolding: Tuple[int, ...] = DEFAULT_SUBFOLDING - stage: Optional[str] = None - - # S3-specific fields - endpoint: Optional[str] = None - bucket: Optional[str] = None - access_key: Optional[str] = None - secret_key: Optional[str] = None - secure: bool = True - proxy_server: Optional[str] = None - - @model_validator(mode="after") - def validate_s3_fields(self) -> "StoreSpec": - """Ensure S3-specific fields are provided for S3 protocol.""" - if self.protocol == "s3": - required = ["endpoint", "bucket", "access_key", "secret_key"] - missing = [f for f in required if getattr(self, f) is None] - if missing: - raise ValueError(f"S3 store requires: {', '.join(missing)}") - return self - - class Config(BaseSettings): """ Main DataJoint configuration. + Settings are loaded from (in priority order): + 1. Environment variables (DJ_*) + 2. Secrets directory (.secrets/ or /run/secrets/datajoint/) + 3. Config file (datajoint.json, searched in parent directories) + 4. Default values + Access settings via attributes: >>> config.database.host >>> config.safemode @@ -173,6 +241,10 @@ class Config(BaseSettings): cache: Optional[Path] = None query_cache: Optional[Path] = None + # Internal: track where config was loaded from + _config_path: Optional[Path] = None + _secrets_dir: Optional[Path] = None + @field_validator("loglevel", mode="after") @classmethod def set_logger_level(cls, v: str) -> str: @@ -243,38 +315,35 @@ def get_store_spec(self, store: str) -> Dict[str, Any]: return spec - def save(self, filename: Union[str, Path], verbose: bool = False) -> None: + def save(self, filename: Optional[Union[str, Path]] = None, verbose: bool = False) -> None: """ Save settings to a JSON file. Args: - filename: Path to save the configuration - verbose: If True, log the save operation + filename: Path to save the configuration. Defaults to datajoint.json in cwd. + verbose: If True, log the save operation. """ + if filename is None: + filename = Path.cwd() / CONFIG_FILENAME + data = self._to_flat_dict() + # Remove secrets from saved config + secrets_keys = ["database.password", "external.aws_secret_access_key"] + for key in secrets_keys: + data.pop(key, None) + with open(filename, "w") as f: json.dump(data, f, indent=4, default=str) if verbose: logger.info(f"Saved settings to {filename}") - def save_local(self, verbose: bool = False) -> None: - """Save settings to local config file (dj_local_conf.json).""" - self.save(LOCALCONFIG, verbose) - - def save_global(self, verbose: bool = False) -> None: - """Save settings to global config file (~/.datajoint_config.json).""" - self.save(Path.home() / GLOBALCONFIG, verbose) - - def load(self, filename: Union[str, Path, None] = None) -> None: + def load(self, filename: Union[str, Path]) -> None: """ Load settings from a JSON file. Args: - filename: Path to load configuration from. If None, uses LOCALCONFIG. + filename: Path to load configuration from. """ - if filename is None: - filename = LOCALCONFIG - filepath = Path(filename) if not filepath.exists(): raise FileNotFoundError(f"Config file not found: {filepath}") @@ -285,6 +354,7 @@ def load(self, filename: Union[str, Path, None] = None) -> None: data = json.load(f) self._update_from_flat_dict(data) + self._config_path = filepath def _to_flat_dict(self) -> Dict[str, Any]: """Convert settings to flat dict with dot notation keys.""" @@ -293,10 +363,14 @@ def _to_flat_dict(self) -> Dict[str, Any]: def flatten(obj: Any, prefix: str = "") -> None: if isinstance(obj, BaseSettings): for name in obj.model_fields: + if name.startswith("_"): + continue value = getattr(obj, name) key = f"{prefix}.{name}" if prefix else name if isinstance(value, BaseSettings): flatten(value, key) + elif isinstance(value, SecretStr): + result[key] = value.get_secret_value() if value else None elif isinstance(value, Path): result[key] = str(value) else: @@ -312,7 +386,7 @@ def _update_from_flat_dict(self, data: Dict[str, Any]) -> None: for key, value in data.items(): parts = key.split(".") if len(parts) == 1: - if hasattr(self, key): + if hasattr(self, key) and not key.startswith("_"): setattr(self, key, value) elif len(parts) == 2: group, attr = parts @@ -321,6 +395,27 @@ def _update_from_flat_dict(self, data: Dict[str, Any]) -> None: if hasattr(group_obj, attr): setattr(group_obj, attr, value) + def _load_secrets(self, secrets_dir: Path) -> None: + """Load secrets from a secrets directory.""" + self._secrets_dir = secrets_dir + + # Map of secret file names to config paths + secret_mappings = { + "database.password": ("database", "password"), + "database.user": ("database", "user"), + "aws.access_key_id": ("external", "aws_access_key_id"), + "aws.secret_access_key": ("external", "aws_secret_access_key"), + } + + for secret_name, (group, attr) in secret_mappings.items(): + value = read_secret_file(secrets_dir, secret_name) + if value is not None: + group_obj = getattr(self, group) + # Only set if not already set by env var + if getattr(group_obj, attr) is None: + setattr(group_obj, attr, value) + logger.debug(f"Loaded secret '{secret_name}' from {secrets_dir}") + @contextmanager def override(self, **kwargs: Any) -> Iterator["Config"]: """ @@ -378,7 +473,7 @@ def override(self, **kwargs: Any) -> Iterator["Config"]: group, attr = key_parts setattr(getattr(self, group), attr, original) - # Backward compatibility: dict-like access + # Dict-like access for convenience def __getitem__(self, key: str) -> Any: """Get setting by dot-notation key (e.g., 'database.host').""" parts = key.split(".") @@ -390,6 +485,9 @@ def __getitem__(self, key: str) -> Any: obj = obj[part] else: raise KeyError(f"Setting '{key}' not found") + # Unwrap SecretStr for compatibility + if isinstance(obj, SecretStr): + return obj.get_secret_value() return obj def __setitem__(self, key: str, value: Any) -> None: @@ -418,21 +516,25 @@ def _create_config() -> Config: """Create and initialize the global config instance.""" cfg = Config() - # Try to load from config file - config_paths = [ - Path(LOCALCONFIG), - Path.home() / GLOBALCONFIG, - ] - - for path in config_paths: - if path.exists(): - try: - cfg.load(path) - break - except Exception as e: - logger.warning(f"Failed to load config from {path}: {e}") + # Find config file (recursive parent search) + config_path = find_config_file() + + if config_path is not None: + try: + cfg.load(config_path) + except Exception as e: + warnings.warn(f"Failed to load config from {config_path}: {e}") else: - logger.debug("No config file found, using defaults and environment variables") + warnings.warn( + f"No {CONFIG_FILENAME} found. Using defaults and environment variables. " + f"Create {CONFIG_FILENAME} in your project root to configure DataJoint.", + stacklevel=2, + ) + + # Find and load secrets + secrets_dir = find_secrets_dir(config_path) + if secrets_dir is not None: + cfg._load_secrets(secrets_dir) # Set initial log level logger.setLevel(cfg.loglevel) diff --git a/tests/test_settings.py b/tests/test_settings.py index ad9d968ab..b07df2865 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,15 +1,158 @@ """Tests for DataJoint settings module.""" +import json import os import tempfile from pathlib import Path import pytest -from pydantic import ValidationError +from pydantic import SecretStr, ValidationError import datajoint as dj from datajoint import settings from datajoint.errors import DataJointError +from datajoint.settings import ( + CONFIG_FILENAME, + SECRETS_DIRNAME, + find_config_file, + find_secrets_dir, + read_secret_file, +) + + +class TestConfigFileSearch: + """Test recursive config file search.""" + + def test_find_in_current_directory(self, tmp_path): + """Config file in current directory is found.""" + config_file = tmp_path / CONFIG_FILENAME + config_file.write_text("{}") + + found = find_config_file(tmp_path) + assert found == config_file + + def test_find_in_parent_directory(self, tmp_path): + """Config file in parent directory is found.""" + subdir = tmp_path / "src" / "pipeline" + subdir.mkdir(parents=True) + config_file = tmp_path / CONFIG_FILENAME + config_file.write_text("{}") + + found = find_config_file(subdir) + assert found == config_file + + def test_stop_at_git_boundary(self, tmp_path): + """Search stops at .git directory.""" + (tmp_path / ".git").mkdir() + subdir = tmp_path / "src" + subdir.mkdir() + # No config file - should return None, not search above .git + + found = find_config_file(subdir) + assert found is None + + def test_stop_at_hg_boundary(self, tmp_path): + """Search stops at .hg directory.""" + (tmp_path / ".hg").mkdir() + subdir = tmp_path / "src" + subdir.mkdir() + + found = find_config_file(subdir) + assert found is None + + def test_config_found_before_git(self, tmp_path): + """Config file found before reaching .git boundary.""" + (tmp_path / ".git").mkdir() + config_file = tmp_path / CONFIG_FILENAME + config_file.write_text("{}") + subdir = tmp_path / "src" + subdir.mkdir() + + found = find_config_file(subdir) + assert found == config_file + + def test_returns_none_when_not_found(self, tmp_path): + """Returns None when no config file exists.""" + (tmp_path / ".git").mkdir() # Create boundary + subdir = tmp_path / "src" + subdir.mkdir() + + found = find_config_file(subdir) + assert found is None + + +class TestSecretsDirectory: + """Test secrets directory detection and loading.""" + + def test_find_secrets_next_to_config(self, tmp_path): + """Finds .secrets/ directory next to config file.""" + config_file = tmp_path / CONFIG_FILENAME + config_file.write_text("{}") + secrets_dir = tmp_path / SECRETS_DIRNAME + secrets_dir.mkdir() + + found = find_secrets_dir(config_file) + assert found == secrets_dir + + def test_no_secrets_dir_returns_none(self, tmp_path): + """Returns None when no secrets directory exists.""" + config_file = tmp_path / CONFIG_FILENAME + config_file.write_text("{}") + + found = find_secrets_dir(config_file) + # May return system secrets dir if it exists, otherwise None + if found is not None: + assert found == settings.SYSTEM_SECRETS_DIR + + def test_read_secret_file(self, tmp_path): + """Reads secret value from file.""" + (tmp_path / "database.password").write_text("my_secret\n") + + value = read_secret_file(tmp_path, "database.password") + assert value == "my_secret" # Strips whitespace + + def test_read_missing_secret_returns_none(self, tmp_path): + """Returns None for missing secret file.""" + value = read_secret_file(tmp_path, "nonexistent") + assert value is None + + def test_read_secret_from_none_dir(self): + """Returns None when secrets_dir is None.""" + value = read_secret_file(None, "database.password") + assert value is None + + +class TestSecretStr: + """Test SecretStr handling for sensitive fields.""" + + def test_password_is_secret_str(self): + """Password field uses SecretStr type.""" + dj.config.database.password = "test_password" + assert isinstance(dj.config.database.password, SecretStr) + dj.config.database.password = None + + def test_secret_str_masked_in_repr(self): + """SecretStr values are masked in repr.""" + dj.config.database.password = "super_secret" + repr_str = repr(dj.config.database.password) + assert "super_secret" not in repr_str + assert "**" in repr_str + dj.config.database.password = None + + def test_dict_access_unwraps_secret(self): + """Dict-style access returns plain string for secrets.""" + dj.config.database.password = "unwrapped_secret" + value = dj.config["database.password"] + assert value == "unwrapped_secret" + assert isinstance(value, str) + assert not isinstance(value, SecretStr) + dj.config.database.password = None + + def test_aws_secret_key_is_secret_str(self): + """AWS secret key uses SecretStr type.""" + dj.config.external.aws_secret_access_key = "aws_secret" + assert isinstance(dj.config.external.aws_secret_access_key, SecretStr) + dj.config.external.aws_secret_access_key = None class TestSettingsAccess: @@ -55,16 +198,6 @@ def test_dict_style_assignment(self): finally: dj.config["database.host"] = original - def test_nested_assignment(self): - """Test setting nested values.""" - original = dj.config.display.limit - try: - dj.config.display.limit = 25 - assert dj.config.display.limit == 25 - assert dj.config["display.limit"] == 25 - finally: - dj.config.display.limit = original - class TestTypeValidation: """Test pydantic type validation.""" @@ -84,16 +217,6 @@ def test_fetch_format_validation(self): with pytest.raises(ValidationError): dj.config.fetch_format = "invalid" - def test_valid_loglevel_values(self): - """Test setting valid log levels.""" - original = dj.config.loglevel - try: - for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: - dj.config.loglevel = level - assert dj.config.loglevel == level - finally: - dj.config.loglevel = original - class TestContextManager: """Test the override context manager.""" @@ -112,16 +235,6 @@ def test_override_nested_value(self): assert dj.config.database.host == "override_host" assert dj.config.database.host == original - def test_override_multiple_values(self): - """Test overriding multiple values at once.""" - orig_safe = dj.config.safemode - orig_host = dj.config.database.host - with dj.config.override(safemode=False, database__host="multi_test"): - assert dj.config.safemode is False - assert dj.config.database.host == "multi_test" - assert dj.config.safemode == orig_safe - assert dj.config.database.host == orig_host - def test_override_restores_on_exception(self): """Test that override restores values even when exception occurs.""" original = dj.config.safemode @@ -137,47 +250,48 @@ def test_override_restores_on_exception(self): class TestSaveLoad: """Test saving and loading configuration.""" - def test_save_and_load(self): + def test_save_and_load(self, tmp_path): """Test saving and loading configuration.""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: - filename = f.name + filename = tmp_path / "test_config.json" + original_host = dj.config.database.host try: - # Modify and save - original_host = dj.config.database.host dj.config.database.host = "saved_host" dj.config.save(filename) - - # Reset and load dj.config.database.host = "reset_host" dj.config.load(filename) assert dj.config.database.host == "saved_host" finally: dj.config.database.host = original_host - os.unlink(filename) - def test_save_local(self): - """Test save_local creates local config file.""" - backup_path = None - if os.path.exists(settings.LOCALCONFIG): - backup_path = settings.LOCALCONFIG + ".backup" - os.rename(settings.LOCALCONFIG, backup_path) + def test_save_excludes_secrets(self, tmp_path): + """Test that save() excludes secret values.""" + filename = tmp_path / "test_config.json" + original_password = dj.config.database.password try: - dj.config.save_local() - assert os.path.exists(settings.LOCALCONFIG) + dj.config.database.password = "should_not_save" + dj.config.save(filename) + + with open(filename) as f: + saved = json.load(f) + + assert "database.password" not in saved finally: - if os.path.exists(settings.LOCALCONFIG): - os.remove(settings.LOCALCONFIG) - if backup_path and os.path.exists(backup_path): - os.rename(backup_path, settings.LOCALCONFIG) + dj.config.database.password = original_password def test_load_nonexistent_file(self): """Test loading nonexistent file raises FileNotFoundError.""" with pytest.raises(FileNotFoundError): dj.config.load("/nonexistent/path/config.json") + def test_save_default_filename(self, tmp_path, monkeypatch): + """Test save() uses datajoint.json in cwd by default.""" + monkeypatch.chdir(tmp_path) + dj.config.save() + assert (tmp_path / CONFIG_FILENAME).exists() + class TestStoreSpec: """Test external store configuration.""" @@ -215,20 +329,6 @@ def test_get_store_spec_missing_required(self): finally: dj.config.stores = original_stores - def test_get_store_spec_invalid_key(self): - """Test invalid keys in store spec raises error.""" - original_stores = dj.config.stores.copy() - try: - dj.config.stores["bad_store"] = { - "protocol": "file", - "location": "/tmp/test", - "invalid_key": "value", - } - with pytest.raises(DataJointError, match="Invalid"): - dj.config.get_store_spec("bad_store") - finally: - dj.config.stores = original_stores - class TestDisplaySettings: """Test display-related settings.""" @@ -242,15 +342,6 @@ def test_display_limit(self): finally: dj.config.display.limit = original - def test_display_width(self): - """Test display width setting.""" - original = dj.config.display.width - try: - dj.config.display.width = 20 - assert dj.config.display.width == 20 - finally: - dj.config.display.width = original - class TestCachePaths: """Test cache path settings.""" @@ -272,12 +363,3 @@ def test_cache_path_none(self): assert dj.config.cache is None finally: dj.config.cache = original - - def test_query_cache_path(self): - """Test query cache path setting.""" - original = dj.config.query_cache - try: - dj.config.query_cache = "/tmp/query_cache" - assert dj.config.query_cache == Path("/tmp/query_cache") - finally: - dj.config.query_cache = original From df452863c73da976c541710a7279203e60fe7149 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 00:17:30 +0000 Subject: [PATCH 032/219] fix: remove unused imports (ruff) --- src/datajoint/settings.py | 2 +- tests/test_settings.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 7b2ef3fa4..448f4e7a6 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -35,7 +35,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, Literal, Optional, Tuple, Union -from pydantic import Field, SecretStr, field_validator, model_validator +from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict from .errors import DataJointError diff --git a/tests/test_settings.py b/tests/test_settings.py index b07df2865..05b1d39d4 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,8 +1,6 @@ """Tests for DataJoint settings module.""" import json -import os -import tempfile from pathlib import Path import pytest From 898c5c2cbf4b92c562ee55a0e647fb95ebf8e570 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 00:19:21 +0000 Subject: [PATCH 033/219] style: apply ruff-format changes --- src/datajoint/settings.py | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 448f4e7a6..43510402e 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -184,12 +184,8 @@ class ExternalSettings(BaseSettings): validate_assignment=True, ) - aws_access_key_id: Optional[str] = Field( - default=None, validation_alias="DJ_AWS_ACCESS_KEY_ID" - ) - aws_secret_access_key: Optional[SecretStr] = Field( - default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY" - ) + aws_access_key_id: Optional[str] = Field(default=None, validation_alias="DJ_AWS_ACCESS_KEY_ID") + aws_secret_access_key: Optional[SecretStr] = Field(default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY") class Config(BaseSettings): @@ -225,9 +221,7 @@ class Config(BaseSettings): external: ExternalSettings = Field(default_factory=ExternalSettings) # Top-level settings - loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field( - default="INFO", validation_alias="DJ_LOG_LEVEL" - ) + loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default="INFO", validation_alias="DJ_LOG_LEVEL") safemode: bool = True fetch_format: Literal["array", "frame"] = "array" enable_python_native_blobs: bool = True @@ -282,9 +276,7 @@ def get_store_spec(self, store: str) -> Dict[str, Any]: # Validate protocol protocol = spec.get("protocol", "").lower() if protocol not in ("file", "s3"): - raise DataJointError( - f'Missing or invalid protocol in config.stores["{store}"]' - ) + raise DataJointError(f'Missing or invalid protocol in config.stores["{store}"]') # Define required and allowed keys by protocol required_keys: Dict[str, Tuple[str, ...]] = { @@ -294,24 +286,28 @@ def get_store_spec(self, store: str) -> Dict[str, Any]: allowed_keys: Dict[str, Tuple[str, ...]] = { "file": ("protocol", "location", "subfolding", "stage"), "s3": ( - "protocol", "endpoint", "bucket", "access_key", "secret_key", - "location", "secure", "subfolding", "stage", "proxy_server", + "protocol", + "endpoint", + "bucket", + "access_key", + "secret_key", + "location", + "secure", + "subfolding", + "stage", + "proxy_server", ), } # Check required keys missing = [k for k in required_keys[protocol] if k not in spec] if missing: - raise DataJointError( - f'config.stores["{store}"] is missing: {", ".join(missing)}' - ) + raise DataJointError(f'config.stores["{store}"] is missing: {", ".join(missing)}') # Check for invalid keys invalid = [k for k in spec if k not in allowed_keys[protocol]] if invalid: - raise DataJointError( - f'Invalid key(s) in config.stores["{store}"]: {", ".join(invalid)}' - ) + raise DataJointError(f'Invalid key(s) in config.stores["{store}"]: {", ".join(invalid)}') return spec From aefb7cf03318054fa521432768136e59a91c54f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 00:46:55 +0000 Subject: [PATCH 034/219] feat: add type aliases for numeric column types Add convenient type aliases that map to MySQL types: - float32 -> float - float64 -> double - int32 -> int - uint32 -> int unsigned - int16 -> smallint - uint16 -> smallint unsigned - int8 -> tinyint - uint8 -> tinyint unsigned These aliases follow the same pattern as UUID, storing the original type in the column comment for round-tripping. --- src/datajoint/declare.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index e706347c9..f2c549037 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -15,6 +15,18 @@ from .settings import config UUID_DATA_TYPE = "binary(16)" + +# Type aliases for numeric types +TYPE_ALIASES = { + "FLOAT32": "float", + "FLOAT64": "double", + "INT32": "int", + "UINT32": "int unsigned", + "INT16": "smallint", + "UINT16": "smallint unsigned", + "INT8": "tinyint", + "UINT8": "tinyint unsigned", +} MAX_TABLE_NAME_LENGTH = 64 CONSTANT_LITERALS = { "CURRENT_TIMESTAMP", @@ -25,6 +37,16 @@ TYPE_PATTERN = { k: re.compile(v, re.I) for k, v in dict( + # Type aliases must come before INTEGER and FLOAT patterns to avoid prefix matching + FLOAT32=r"float32$", + FLOAT64=r"float64$", + INT32=r"int32$", + UINT32=r"uint32$", + INT16=r"int16$", + UINT16=r"uint16$", + INT8=r"int8$", + UINT8=r"uint8$", + # Native MySQL types INTEGER=r"((tiny|small|medium|big|)int|integer)(\s*\(.+\))?(\s+unsigned)?(\s+auto_increment)?|serial$", DECIMAL=r"(decimal|numeric)(\s*\(.+\))?(\s+unsigned)?$", FLOAT=r"(double|float|real)(\s*\(.+\))?(\s+unsigned)?$", @@ -51,7 +73,7 @@ "EXTERNAL_BLOB", "FILEPATH", "ADAPTED", -} +} | set(TYPE_ALIASES) NATIVE_TYPES = set(TYPE_PATTERN) - SPECIAL_TYPES EXTERNAL_TYPES = { "EXTERNAL_ATTACH", @@ -460,6 +482,8 @@ def substitute_special_type(match, category, foreign_key_sql, context): if category in SPECIAL_TYPES: # recursive redefinition from user-defined datatypes. substitute_special_type(match, category, foreign_key_sql, context) + elif category in TYPE_ALIASES: + match["type"] = TYPE_ALIASES[category] else: assert False, "Unknown special type" From 864121dfdd1d4c679ce496b0ad32129660c7a574 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 00:50:24 +0000 Subject: [PATCH 035/219] feat: add int64 and uint64 type aliases - int64 -> bigint - uint64 -> bigint unsigned --- src/datajoint/declare.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index f2c549037..c1a22f0ca 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -20,6 +20,8 @@ TYPE_ALIASES = { "FLOAT32": "float", "FLOAT64": "double", + "INT64": "bigint", + "UINT64": "bigint unsigned", "INT32": "int", "UINT32": "int unsigned", "INT16": "smallint", @@ -40,6 +42,8 @@ # Type aliases must come before INTEGER and FLOAT patterns to avoid prefix matching FLOAT32=r"float32$", FLOAT64=r"float64$", + INT64=r"int64$", + UINT64=r"uint64$", INT32=r"int32$", UINT32=r"uint32$", INT16=r"int16$", From ca7f078be44f33518418f81e07d44015ff159fb6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 00:56:53 +0000 Subject: [PATCH 036/219] test: add tests for numeric type aliases Add comprehensive tests for the new type aliases feature: - Pattern matching tests for all 10 type aliases - MySQL type mapping verification - Table creation with type aliases - Insert and fetch operations - Primary key usage with type aliases - Nullable column support --- tests/conftest.py | 16 ++++ tests/schema_type_aliases.py | 49 ++++++++++ tests/test_type_aliases.py | 179 +++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 tests/schema_type_aliases.py create mode 100644 tests/test_type_aliases.py diff --git a/tests/conftest.py b/tests/conftest.py index a118228c5..8a6ba4057 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,6 +23,7 @@ from . import schema, schema_advanced, schema_external, schema_simple from . import schema_uuid as schema_uuid_module +from . import schema_type_aliases as schema_type_aliases_module # Configure logging for container management logger = logging.getLogger(__name__) @@ -771,6 +772,21 @@ def schema_uuid(connection_test, prefix): schema.drop() +@pytest.fixture(scope="module") +def schema_type_aliases(connection_test, prefix): + """Schema for testing numeric type aliases.""" + schema = dj.Schema( + prefix + "_type_aliases", + context=schema_type_aliases_module.LOCALS_TYPE_ALIASES, + connection=connection_test, + ) + schema(schema_type_aliases_module.TypeAliasTable) + schema(schema_type_aliases_module.TypeAliasPrimaryKey) + schema(schema_type_aliases_module.TypeAliasNullable) + yield schema + schema.drop() + + @pytest.fixture(scope="session") def http_client(): # Initialize httpClient with relevant timeout. diff --git a/tests/schema_type_aliases.py b/tests/schema_type_aliases.py new file mode 100644 index 000000000..cdd558868 --- /dev/null +++ b/tests/schema_type_aliases.py @@ -0,0 +1,49 @@ +""" +Schema for testing numeric type aliases. +""" + +import inspect + +import datajoint as dj + + +class TypeAliasTable(dj.Manual): + definition = """ + # Table with all numeric type aliases + id : int + --- + val_float32 : float32 # 32-bit float + val_float64 : float64 # 64-bit float + val_int64 : int64 # 64-bit signed integer + val_uint64 : uint64 # 64-bit unsigned integer + val_int32 : int32 # 32-bit signed integer + val_uint32 : uint32 # 32-bit unsigned integer + val_int16 : int16 # 16-bit signed integer + val_uint16 : uint16 # 16-bit unsigned integer + val_int8 : int8 # 8-bit signed integer + val_uint8 : uint8 # 8-bit unsigned integer + """ + + +class TypeAliasPrimaryKey(dj.Manual): + definition = """ + # Table with type alias in primary key + pk_int32 : int32 + pk_uint16 : uint16 + --- + value : varchar(100) + """ + + +class TypeAliasNullable(dj.Manual): + definition = """ + # Table with nullable type alias columns + id : int + --- + nullable_float32 = null : float32 + nullable_int64 = null : int64 + """ + + +LOCALS_TYPE_ALIASES = {k: v for k, v in locals().items() if inspect.isclass(v)} +__all__ = list(LOCALS_TYPE_ALIASES) diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py new file mode 100644 index 000000000..a97876939 --- /dev/null +++ b/tests/test_type_aliases.py @@ -0,0 +1,179 @@ +""" +Tests for numeric type aliases (float32, float64, int8, int16, int32, int64, etc.) +""" + +import pytest + +from datajoint.declare import TYPE_ALIASES, TYPE_PATTERN, SPECIAL_TYPES, match_type + +from .schema_type_aliases import TypeAliasTable, TypeAliasPrimaryKey, TypeAliasNullable + + +class TestTypeAliasPatterns: + """Test that type alias patterns are correctly defined and matched.""" + + @pytest.mark.parametrize( + "alias,expected_category", + [ + ("float32", "FLOAT32"), + ("float64", "FLOAT64"), + ("int64", "INT64"), + ("uint64", "UINT64"), + ("int32", "INT32"), + ("uint32", "UINT32"), + ("int16", "INT16"), + ("uint16", "UINT16"), + ("int8", "INT8"), + ("uint8", "UINT8"), + ], + ) + def test_type_alias_pattern_matching(self, alias, expected_category): + """Test that type aliases are matched to correct categories.""" + category = match_type(alias) + assert category == expected_category + assert category in SPECIAL_TYPES + assert category in TYPE_ALIASES + + @pytest.mark.parametrize( + "alias,expected_mysql_type", + [ + ("float32", "float"), + ("float64", "double"), + ("int64", "bigint"), + ("uint64", "bigint unsigned"), + ("int32", "int"), + ("uint32", "int unsigned"), + ("int16", "smallint"), + ("uint16", "smallint unsigned"), + ("int8", "tinyint"), + ("uint8", "tinyint unsigned"), + ], + ) + def test_type_alias_mysql_mapping(self, alias, expected_mysql_type): + """Test that type aliases map to correct MySQL types.""" + category = match_type(alias) + mysql_type = TYPE_ALIASES[category] + assert mysql_type == expected_mysql_type + + @pytest.mark.parametrize( + "native_type,expected_category", + [ + ("int", "INTEGER"), + ("bigint", "INTEGER"), + ("smallint", "INTEGER"), + ("tinyint", "INTEGER"), + ("float", "FLOAT"), + ("double", "FLOAT"), + ], + ) + def test_native_types_still_work(self, native_type, expected_category): + """Test that native MySQL types still match correctly.""" + category = match_type(native_type) + assert category == expected_category + + +class TestTypeAliasTableCreation: + """Test table creation with type aliases.""" + + def test_create_table_with_all_aliases(self, schema_type_aliases): + """Test that tables with all type aliases can be created.""" + assert TypeAliasTable().full_table_name is not None + + def test_create_table_with_alias_primary_key(self, schema_type_aliases): + """Test that tables with type aliases in primary key can be created.""" + assert TypeAliasPrimaryKey().full_table_name is not None + + def test_create_table_with_nullable_aliases(self, schema_type_aliases): + """Test that tables with nullable type alias columns can be created.""" + assert TypeAliasNullable().full_table_name is not None + + +class TestTypeAliasHeading: + """Test that headings correctly preserve type alias information.""" + + def test_heading_preserves_type_aliases(self, schema_type_aliases): + """Test that heading shows original type aliases.""" + heading = TypeAliasTable().heading + heading_str = repr(heading) + + # Check that type aliases appear in the heading representation + assert "float32" in heading_str + assert "float64" in heading_str + assert "int64" in heading_str + assert "uint64" in heading_str + assert "int32" in heading_str + assert "uint32" in heading_str + assert "int16" in heading_str + assert "uint16" in heading_str + assert "int8" in heading_str + assert "uint8" in heading_str + + +class TestTypeAliasInsertFetch: + """Test inserting and fetching data with type aliases.""" + + def test_insert_and_fetch(self, schema_type_aliases): + """Test inserting and fetching values with type aliases.""" + table = TypeAliasTable() + table.delete() + + test_data = dict( + id=1, + val_float32=3.14, + val_float64=2.718281828, + val_int64=9223372036854775807, # max int64 + val_uint64=18446744073709551615, # max uint64 + val_int32=2147483647, # max int32 + val_uint32=4294967295, # max uint32 + val_int16=32767, # max int16 + val_uint16=65535, # max uint16 + val_int8=127, # max int8 + val_uint8=255, # max uint8 + ) + + table.insert1(test_data) + fetched = table.fetch1() + + assert fetched["id"] == test_data["id"] + assert abs(fetched["val_float32"] - test_data["val_float32"]) < 0.001 + assert abs(fetched["val_float64"] - test_data["val_float64"]) < 1e-9 + assert fetched["val_int64"] == test_data["val_int64"] + assert fetched["val_uint64"] == test_data["val_uint64"] + assert fetched["val_int32"] == test_data["val_int32"] + assert fetched["val_uint32"] == test_data["val_uint32"] + assert fetched["val_int16"] == test_data["val_int16"] + assert fetched["val_uint16"] == test_data["val_uint16"] + assert fetched["val_int8"] == test_data["val_int8"] + assert fetched["val_uint8"] == test_data["val_uint8"] + + def test_insert_primary_key_with_aliases(self, schema_type_aliases): + """Test using type aliases in primary key.""" + table = TypeAliasPrimaryKey() + table.delete() + + table.insert1(dict(pk_int32=100, pk_uint16=200, value="test")) + fetched = (table & dict(pk_int32=100, pk_uint16=200)).fetch1() + + assert fetched["pk_int32"] == 100 + assert fetched["pk_uint16"] == 200 + assert fetched["value"] == "test" + + def test_nullable_type_aliases(self, schema_type_aliases): + """Test nullable columns with type aliases.""" + table = TypeAliasNullable() + table.delete() + + # Insert with NULL values + table.insert1(dict(id=1, nullable_float32=None, nullable_int64=None)) + fetched = table.fetch1() + + assert fetched["id"] == 1 + assert fetched["nullable_float32"] is None + assert fetched["nullable_int64"] is None + + # Insert with actual values + table.insert1(dict(id=2, nullable_float32=1.5, nullable_int64=999)) + fetched = (table & dict(id=2)).fetch1() + + assert fetched["nullable_float32"] == 1.5 + assert fetched["nullable_int64"] == 999 From 36a553a48b99e5d7887defc463fabb2afa3291c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 01:02:10 +0000 Subject: [PATCH 037/219] fix: remove unused import in test_type_aliases --- tests/test_type_aliases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index a97876939..436d608bf 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -4,7 +4,7 @@ import pytest -from datajoint.declare import TYPE_ALIASES, TYPE_PATTERN, SPECIAL_TYPES, match_type +from datajoint.declare import TYPE_ALIASES, SPECIAL_TYPES, match_type from .schema_type_aliases import TypeAliasTable, TypeAliasPrimaryKey, TypeAliasNullable From 43bd053edd3e1a1023fc6c80f5e37e1a8c925872 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 14:19:45 +0000 Subject: [PATCH 038/219] refactor: remove set_password from admin module Credentials should be managed via environment variables or .secrets/ directory, not stored in config files. --- src/datajoint/__init__.py | 3 +-- src/datajoint/admin.py | 25 ------------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/src/datajoint/__init__.py b/src/datajoint/__init__.py index a7c5e7b2f..0f8123c66 100644 --- a/src/datajoint/__init__.py +++ b/src/datajoint/__init__.py @@ -42,7 +42,6 @@ "Diagram", "Di", "ERD", - "set_password", "kill", "MatCell", "MatStruct", @@ -56,7 +55,7 @@ ] from . import errors -from .admin import kill, set_password +from .admin import kill from .attribute_adapter import AttributeAdapter from .blob import MatCell, MatStruct from .cli import cli diff --git a/src/datajoint/admin.py b/src/datajoint/admin.py index c5e93f88f..64a91bb48 100644 --- a/src/datajoint/admin.py +++ b/src/datajoint/admin.py @@ -1,37 +1,12 @@ import logging -from getpass import getpass import pymysql -from packaging import version from .connection import conn -from .settings import config -from .utils import user_choice logger = logging.getLogger(__name__.split(".")[0]) -def set_password(new_password=None, connection=None, update_config=None): - connection = conn() if connection is None else connection - if new_password is None: - new_password = getpass("New password: ") - confirm_password = getpass("Confirm password: ") - if new_password != confirm_password: - logger.warning("Failed to confirm the password! Aborting password change.") - return - - if version.parse(connection.query("select @@version;").fetchone()[0]) >= version.parse("5.7"): - # SET PASSWORD is deprecated as of MySQL 5.7 and removed in 8+ - connection.query("ALTER USER user() IDENTIFIED BY '%s';" % new_password) - else: - connection.query("SET PASSWORD = PASSWORD('%s')" % new_password) - logger.info("Password updated.") - - if update_config or (update_config is None and user_choice("Update local setting?") == "yes"): - config["database.password"] = new_password - config.save_local(verbose=True) - - def kill(restriction=None, connection=None, order_by=None): """ view and kill database connections. From 97107f6c2f124682ffbd4702246fe13530175ccc Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 15:37:50 +0000 Subject: [PATCH 039/219] refactor: remove save methods from settings Config files should be created/edited manually and version controlled, not generated programmatically. --- src/datajoint/settings.py | 47 --------------------------------------- tests/test_settings.py | 38 +++++-------------------------- 2 files changed, 6 insertions(+), 79 deletions(-) diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 43510402e..c5719dae3 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -311,28 +311,6 @@ def get_store_spec(self, store: str) -> Dict[str, Any]: return spec - def save(self, filename: Optional[Union[str, Path]] = None, verbose: bool = False) -> None: - """ - Save settings to a JSON file. - - Args: - filename: Path to save the configuration. Defaults to datajoint.json in cwd. - verbose: If True, log the save operation. - """ - if filename is None: - filename = Path.cwd() / CONFIG_FILENAME - - data = self._to_flat_dict() - # Remove secrets from saved config - secrets_keys = ["database.password", "external.aws_secret_access_key"] - for key in secrets_keys: - data.pop(key, None) - - with open(filename, "w") as f: - json.dump(data, f, indent=4, default=str) - if verbose: - logger.info(f"Saved settings to {filename}") - def load(self, filename: Union[str, Path]) -> None: """ Load settings from a JSON file. @@ -352,31 +330,6 @@ def load(self, filename: Union[str, Path]) -> None: self._update_from_flat_dict(data) self._config_path = filepath - def _to_flat_dict(self) -> Dict[str, Any]: - """Convert settings to flat dict with dot notation keys.""" - result: Dict[str, Any] = {} - - def flatten(obj: Any, prefix: str = "") -> None: - if isinstance(obj, BaseSettings): - for name in obj.model_fields: - if name.startswith("_"): - continue - value = getattr(obj, name) - key = f"{prefix}.{name}" if prefix else name - if isinstance(value, BaseSettings): - flatten(value, key) - elif isinstance(value, SecretStr): - result[key] = value.get_secret_value() if value else None - elif isinstance(value, Path): - result[key] = str(value) - else: - result[key] = value - elif isinstance(obj, dict): - result[prefix] = obj - - flatten(self) - return result - def _update_from_flat_dict(self, data: Dict[str, Any]) -> None: """Update settings from a flat dict with dot notation keys.""" for key, value in data.items(): diff --git a/tests/test_settings.py b/tests/test_settings.py index 05b1d39d4..da9ac723a 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,6 +1,5 @@ """Tests for DataJoint settings module.""" -import json from pathlib import Path import pytest @@ -245,51 +244,26 @@ def test_override_restores_on_exception(self): assert dj.config.safemode == original -class TestSaveLoad: - """Test saving and loading configuration.""" +class TestLoad: + """Test loading configuration.""" - def test_save_and_load(self, tmp_path): - """Test saving and loading configuration.""" + def test_load_config_file(self, tmp_path): + """Test loading configuration from file.""" filename = tmp_path / "test_config.json" + filename.write_text('{"database": {"host": "loaded_host"}}') original_host = dj.config.database.host try: - dj.config.database.host = "saved_host" - dj.config.save(filename) - dj.config.database.host = "reset_host" dj.config.load(filename) - - assert dj.config.database.host == "saved_host" + assert dj.config.database.host == "loaded_host" finally: dj.config.database.host = original_host - def test_save_excludes_secrets(self, tmp_path): - """Test that save() excludes secret values.""" - filename = tmp_path / "test_config.json" - original_password = dj.config.database.password - - try: - dj.config.database.password = "should_not_save" - dj.config.save(filename) - - with open(filename) as f: - saved = json.load(f) - - assert "database.password" not in saved - finally: - dj.config.database.password = original_password - def test_load_nonexistent_file(self): """Test loading nonexistent file raises FileNotFoundError.""" with pytest.raises(FileNotFoundError): dj.config.load("/nonexistent/path/config.json") - def test_save_default_filename(self, tmp_path, monkeypatch): - """Test save() uses datajoint.json in cwd by default.""" - monkeypatch.chdir(tmp_path) - dj.config.save() - assert (tmp_path / CONFIG_FILENAME).exists() - class TestStoreSpec: """Test external store configuration.""" From 69ed63e6b35d0b060a24d84223df5daffecb5801 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 15:39:59 +0000 Subject: [PATCH 040/219] docs: update documentation for new settings system - Rewrite settings.md with new config structure and secrets management - Update credentials.md to remove set_password and save methods - Update quick-start.md with new config file name and patterns - Add documentation for .secrets/ directory and environment variables --- docs/src/client/credentials.md | 90 ++++++++++++------ docs/src/client/settings.md | 167 +++++++++++++++++++++++++++++++-- docs/src/quick-start.md | 33 +++---- 3 files changed, 239 insertions(+), 51 deletions(-) diff --git a/docs/src/client/credentials.md b/docs/src/client/credentials.md index bac54a6cf..28e685f1f 100644 --- a/docs/src/client/credentials.md +++ b/docs/src/client/credentials.md @@ -1,46 +1,82 @@ # Credentials -Configure the connection through DataJoint's `config` object: +Database credentials should never be stored in config files. Use environment variables or a secrets directory instead. -```python -> import datajoint as dj -DataJoint 0.4.9 (February 1, 2017) -No configuration found. Use `dj.config` to configure and save the configuration. +## Environment Variables (Recommended) + +Set the following environment variables: + +```bash +export DJ_HOST=db.example.com +export DJ_USER=alice +export DJ_PASS=secret ``` -You may now set the database credentials: +These take priority over all other configuration sources. + +## Secrets Directory + +Create a `.secrets/` directory next to your `datajoint.json`: -```python -dj.config['database.host'] = "alicelab.datajoint.io" -dj.config['database.user'] = "alice" -dj.config['database.password'] = "haha not my real password" +``` +myproject/ +├── datajoint.json +└── .secrets/ + ├── database.user # Contains: alice + └── database.password # Contains: secret ``` -Skip setting the password to make DataJoint prompt to enter the password every time. +Each file contains a single secret value (no JSON, just the raw value). -You may save the configuration in the local work directory with -`dj.config.save_local()` or for all your projects in `dj.config.save_global()`. -Configuration changes should be made through the `dj.config` interface; the config file -should not be modified directly by the user. +Add `.secrets/` to your `.gitignore`: -You may leave the user or the password as `None`, in which case you will be prompted to -enter them manually for every session. -Setting the password as an empty string allows access without a password. +``` +# .gitignore +.secrets/ +``` -Note that the system environment variables `DJ_HOST`, `DJ_USER`, and `DJ_PASS` will -overwrite the settings in the config file. -You can use them to set the connection credentials instead of config files. +## Docker / Kubernetes -To change the password, the `dj.set_password` function will walk you through the -process: +Mount secrets at `/run/secrets/datajoint/`: + +```yaml +# docker-compose.yml +services: + app: + volumes: + - ./secrets:/run/secrets/datajoint:ro +``` + +## Interactive Prompt + +If credentials are not provided via environment variables or secrets, DataJoint will prompt for them when connecting: ```python -dj.set_password() +>>> import datajoint as dj +>>> dj.conn() +Please enter DataJoint username: alice +Please enter DataJoint password: ``` -After that, update the password in the configuration and save it as described above: +## Programmatic Access + +You can also set credentials in Python (useful for testing): ```python -dj.config['database.password'] = 'my#cool!new*psswrd' -dj.config.save_local() # or dj.config.save_global() +import datajoint as dj + +dj.config.database.user = "alice" +dj.config.database.password = "secret" +``` + +Note that `password` uses `SecretStr` internally, so it will be masked in logs and repr output. + +## Changing Database Password + +To change your database password, use your database's native tools: + +```sql +ALTER USER 'alice'@'%' IDENTIFIED BY 'new_password'; ``` + +Then update your environment variables or secrets file accordingly. diff --git a/docs/src/client/settings.md b/docs/src/client/settings.md index cb9a69fff..d9fd468a2 100644 --- a/docs/src/client/settings.md +++ b/docs/src/client/settings.md @@ -1,11 +1,166 @@ # Configuration Settings -If you are not using DataJoint on your own, or are setting up a DataJoint -system for other users, some additional configuration options may be required -to support [TLS](#tls-configuration) or -[external storage](../sysadmin/external-store.md). +DataJoint uses a strongly-typed configuration system built on [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). + +## Configuration Sources + +Settings are loaded from the following sources (in priority order): + +1. **Environment variables** (`DJ_*`) +2. **Secrets directory** (`.secrets/` or `/run/secrets/datajoint/`) +3. **Project config file** (`datajoint.json`, searched recursively) +4. **Default values** + +## Project Structure + +``` +myproject/ +├── .git/ +├── datajoint.json # Project config (commit this) +├── .secrets/ # Local secrets (add to .gitignore) +│ ├── database.password +│ └── aws.secret_access_key +└── src/ + └── analysis.py # Config found via parent search +``` + +## Config File + +Create a `datajoint.json` file in your project root: + +```json +{ + "database": { + "host": "db.example.com", + "port": 3306 + }, + "stores": { + "raw": { + "protocol": "file", + "location": "/data/raw" + } + }, + "display": { + "limit": 20 + }, + "safemode": true +} +``` + +DataJoint searches for this file starting from the current directory and moving up through parent directories, stopping at the first `.git` or `.hg` directory (project boundary) or filesystem root. + +## Credentials + +**Never store credentials in config files.** Use one of these methods: + +### Environment Variables (Recommended) + +```bash +export DJ_USER=alice +export DJ_PASS=secret +export DJ_HOST=db.example.com +``` + +### Secrets Directory + +Create files in `.secrets/` next to your `datajoint.json`: + +``` +.secrets/ +├── database.password # Contains: secret +├── database.user # Contains: alice +├── aws.access_key_id +└── aws.secret_access_key +``` + +Add `.secrets/` to your `.gitignore`. + +For Docker/Kubernetes, secrets can be mounted at `/run/secrets/datajoint/`. + +## Accessing Settings + +```python +import datajoint as dj + +# Attribute access (preferred) +dj.config.database.host +dj.config.safemode + +# Dict-style access +dj.config["database.host"] +dj.config["safemode"] +``` + +## Temporary Overrides + +Use the context manager for temporary changes: + +```python +with dj.config.override(safemode=False): + # safemode is False here + table.delete() +# safemode is restored +``` + +For nested settings, use double underscores: + +```python +with dj.config.override(database__host="test.example.com"): + # database.host is temporarily changed + pass +``` + +## Available Settings + +### Database Connection + +| Setting | Environment Variable | Default | Description | +|---------|---------------------|---------|-------------| +| `database.host` | `DJ_HOST` | `localhost` | Database server hostname | +| `database.port` | `DJ_PORT` | `3306` | Database server port | +| `database.user` | `DJ_USER` | `None` | Database username | +| `database.password` | `DJ_PASS` | `None` | Database password (use env/secrets) | +| `database.reconnect` | — | `True` | Auto-reconnect on connection loss | +| `database.use_tls` | — | `None` | TLS mode: `True`, `False`, or `None` (auto) | + +### Display + +| Setting | Default | Description | +|---------|---------|-------------| +| `display.limit` | `12` | Max rows to display in previews | +| `display.width` | `14` | Column width in previews | +| `display.show_tuple_count` | `True` | Show total count in previews | + +### Other Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `safemode` | `True` | Prompt before destructive operations | +| `loglevel` | `INFO` | Logging level | +| `fetch_format` | `array` | Default fetch format (`array` or `frame`) | +| `enable_python_native_blobs` | `True` | Use Python-native blob serialization | ## TLS Configuration -Starting with v0.12, DataJoint will by default use TLS if it is available. TLS can be -forced on or off with the boolean `dj.config['database.use_tls']`. +DataJoint uses TLS by default if available. Control this with: + +```python +dj.config.database.use_tls = True # Require TLS +dj.config.database.use_tls = False # Disable TLS +dj.config.database.use_tls = None # Auto (default) +``` + +## External Storage + +Configure external stores in the `stores` section. See [External Storage](../sysadmin/external-store.md) for details. + +```json +{ + "stores": { + "raw": { + "protocol": "file", + "location": "/data/external" + } + } +} +``` diff --git a/docs/src/quick-start.md b/docs/src/quick-start.md index a7f255658..d52dd50f8 100644 --- a/docs/src/quick-start.md +++ b/docs/src/quick-start.md @@ -92,35 +92,32 @@ Next, please install DataJoint via one of the following: ```python linenums="1" import datajoint as dj - dj.config["database.host"] = "{host_address}" - dj.config["database.user"] = "{user}" - dj.config["database.password"] = "{password}" + dj.config.database.host = "{host_address}" + dj.config.database.user = "{user}" + dj.config.database.password = "{password}" ``` - These configuration settings can be saved either locally or system-wide using one - of the following commands: - ```python - dj.config.save_local() - dj.config.save_global() - ``` + Note: Credentials set this way are not persisted. For persistent configuration, + use environment variables or a config file. === "file" - Before using `datajoint`, create a file named `dj_local_conf.json` in the current - directory like so: + Create a file named `datajoint.json` in your project root: ```json linenums="1" { - "database.host": "{host_address}", - "database.user": "{user}", - "database.password": "{password}" + "database": { + "host": "{host_address}" + } } ``` - These settings will be loaded whenever a Python instance is launched from this - directory. To configure settings globally, save a similar file as - `.datajoint_config.json` in your home directory. A local config, if present, will - take precedent over global settings. + **Important:** Never store credentials in config files. Use environment variables + (`DJ_USER`, `DJ_PASS`) or a `.secrets/` directory instead. + + DataJoint searches for `datajoint.json` starting from the current directory and + moving up through parent directories until it finds the file or reaches a `.git` + directory. ## Data Pipeline Definition From 6fefbf6b41a3b41d7643717cc7a3143e835f34ba Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 15:50:58 +0000 Subject: [PATCH 041/219] docs: add documentation for numeric type aliases Document the new type aliases (float32, float64, int8-64, uint8-64) in the datatypes documentation with a table of mappings and example usage. --- docs/src/design/tables/attributes.md | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/src/design/tables/attributes.md b/docs/src/design/tables/attributes.md index 0c2e7a8f9..9363e527f 100644 --- a/docs/src/design/tables/attributes.md +++ b/docs/src/design/tables/attributes.md @@ -77,6 +77,40 @@ sending/receiving an opaque data file to/from a DataJoint pipeline. - `filepath@store`: a [filepath](filepath.md) used to link non-DataJoint managed files into a DataJoint pipeline. +## Numeric type aliases + +DataJoint provides convenient type aliases that map to standard MySQL numeric types. +These aliases use familiar naming conventions from NumPy and other numerical computing +libraries, making table definitions more readable and explicit about data precision. + +| Alias | MySQL Type | Description | +|-------|------------|-------------| +| `int8` | `tinyint` | 8-bit signed integer (-128 to 127) | +| `uint8` | `tinyint unsigned` | 8-bit unsigned integer (0 to 255) | +| `int16` | `smallint` | 16-bit signed integer (-32,768 to 32,767) | +| `uint16` | `smallint unsigned` | 16-bit unsigned integer (0 to 65,535) | +| `int32` | `int` | 32-bit signed integer | +| `uint32` | `int unsigned` | 32-bit unsigned integer | +| `int64` | `bigint` | 64-bit signed integer | +| `uint64` | `bigint unsigned` | 64-bit unsigned integer | +| `float32` | `float` | 32-bit single-precision floating point | +| `float64` | `double` | 64-bit double-precision floating point | + +Example usage: + +```python +@schema +class Measurement(dj.Manual): + definition = """ + measurement_id : int + --- + temperature : float32 # single-precision temperature reading + precise_value : float64 # double-precision measurement + sample_count : uint32 # unsigned 32-bit counter + sensor_flags : uint8 # 8-bit status flags + """ +``` + ## Datatypes not (yet) supported - `binary` From b55c1bf46677b317bdd5d381b8ae70e40278d8bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 19:58:24 +0000 Subject: [PATCH 042/219] Drop support for Python < 3.10 and MySQL < 8.0 - Update pyproject.toml to require Python >=3.10 (was >=3.9) - Update ruff target-version to py310 - Update pixi Python version constraint - Modernize type hints to use Python 3.10+ union syntax (X | Y instead of Union[X, Y], X | None instead of Optional[X]) - Use built-in dict, list, tuple for generics instead of typing imports - Update MySQL documentation reference from 5.7 to 8.0 --- pyproject.toml | 6 ++--- src/datajoint/condition.py | 5 ++--- src/datajoint/connection.py | 2 +- src/datajoint/settings.py | 44 ++++++++++++++++++------------------- src/datajoint/table.py | 3 +-- 5 files changed, 29 insertions(+), 31 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 74cf1ba5a..dc151d7cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "setuptools", "pydantic-settings>=2.0.0", ] -requires-python = ">=3.9,<3.14" +requires-python = ">=3.10,<3.14" authors = [ {name = "Dimitri Yatsenko", email = "dimitri@datajoint.com"}, {name = "Thinh Nguyen", email = "thinh@datajoint.com"}, @@ -102,7 +102,7 @@ dev = [ [tool.ruff] # Equivalent to flake8 configuration line-length = 127 -target-version = "py39" +target-version = "py310" [tool.ruff.lint] # Enable specific rule sets equivalent to flake8 configuration @@ -176,7 +176,7 @@ test = { features = ["test"], solve-group = "default" } [tool.pixi.tasks] [tool.pixi.dependencies] -python = ">=3.9,<3.14" +python = ">=3.10,<3.14" graphviz = ">=13.1.2,<14" [tool.pixi.activation] diff --git a/src/datajoint/condition.py b/src/datajoint/condition.py index f77cb2a2d..8a22d17bb 100644 --- a/src/datajoint/condition.py +++ b/src/datajoint/condition.py @@ -8,7 +8,6 @@ import re import uuid from dataclasses import dataclass -from typing import List, Union import numpy import pandas @@ -67,8 +66,8 @@ class Top: In SQL, this corresponds to ORDER BY ... LIMIT ... OFFSET """ - limit: Union[int, None] = 1 - order_by: Union[str, List[str]] = "KEY" + limit: int | None = 1 + order_by: str | list[str] = "KEY" offset: int = 0 def __post_init__(self): diff --git a/src/datajoint/connection.py b/src/datajoint/connection.py index 545595fed..66d926694 100644 --- a/src/datajoint/connection.py +++ b/src/datajoint/connection.py @@ -86,7 +86,7 @@ def conn(host=None, user=None, password=None, *, init_fun=None, reset=False, use :param reset: whether the connection should be reset or not :param use_tls: TLS encryption option. Valid options are: True (required), False (required no TLS), None (TLS preferred, default), dict (Manually specify values per - https://dev.mysql.com/doc/refman/5.7/en/connection-options.html#encrypted-connection-options). + https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#encrypted-connection-options). """ if not hasattr(conn, "connection") or reset: host = host if host is not None else config["database.host"] diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index c5719dae3..65b91aa2c 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -33,7 +33,7 @@ from copy import deepcopy from enum import Enum from pathlib import Path -from typing import Any, Dict, Iterator, Literal, Optional, Tuple, Union +from typing import Any, Iterator, Literal from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict @@ -58,7 +58,7 @@ logger = logging.getLogger(__name__.split(".")[0]) -def find_config_file(start: Optional[Path] = None) -> Optional[Path]: +def find_config_file(start: Path | None = None) -> Path | None: """ Search for datajoint.json in current and parent directories. @@ -89,7 +89,7 @@ def find_config_file(start: Optional[Path] = None) -> Optional[Path]: current = current.parent -def find_secrets_dir(config_path: Optional[Path] = None) -> Optional[Path]: +def find_secrets_dir(config_path: Path | None = None) -> Path | None: """ Find the secrets directory. @@ -116,7 +116,7 @@ def find_secrets_dir(config_path: Optional[Path] = None) -> Optional[Path]: return None -def read_secret_file(secrets_dir: Optional[Path], name: str) -> Optional[str]: +def read_secret_file(secrets_dir: Path | None, name: str) -> str | None: """ Read a secret value from a file in the secrets directory. @@ -148,11 +148,11 @@ class DatabaseSettings(BaseSettings): ) host: str = Field(default="localhost", validation_alias="DJ_HOST") - user: Optional[str] = Field(default=None, validation_alias="DJ_USER") - password: Optional[SecretStr] = Field(default=None, validation_alias="DJ_PASS") + user: str | None = Field(default=None, validation_alias="DJ_USER") + password: SecretStr | None = Field(default=None, validation_alias="DJ_PASS") port: int = Field(default=3306, validation_alias="DJ_PORT") reconnect: bool = True - use_tls: Optional[bool] = None + use_tls: bool | None = None class ConnectionSettings(BaseSettings): @@ -160,7 +160,7 @@ class ConnectionSettings(BaseSettings): model_config = SettingsConfigDict(extra="forbid", validate_assignment=True) - init_function: Optional[str] = None + init_function: str | None = None charset: str = "" # pymysql uses '' as default @@ -184,8 +184,8 @@ class ExternalSettings(BaseSettings): validate_assignment=True, ) - aws_access_key_id: Optional[str] = Field(default=None, validation_alias="DJ_AWS_ACCESS_KEY_ID") - aws_secret_access_key: Optional[SecretStr] = Field(default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY") + aws_access_key_id: str | None = Field(default=None, validation_alias="DJ_AWS_ACCESS_KEY_ID") + aws_secret_access_key: SecretStr | None = Field(default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY") class Config(BaseSettings): @@ -226,18 +226,18 @@ class Config(BaseSettings): fetch_format: Literal["array", "frame"] = "array" enable_python_native_blobs: bool = True add_hidden_timestamp: bool = False - filepath_checksum_size_limit: Optional[int] = None + filepath_checksum_size_limit: int | None = None # External stores configuration - stores: Dict[str, Dict[str, Any]] = Field(default_factory=dict) + stores: dict[str, dict[str, Any]] = Field(default_factory=dict) # Cache paths - cache: Optional[Path] = None - query_cache: Optional[Path] = None + cache: Path | None = None + query_cache: Path | None = None # Internal: track where config was loaded from - _config_path: Optional[Path] = None - _secrets_dir: Optional[Path] = None + _config_path: Path | None = None + _secrets_dir: Path | None = None @field_validator("loglevel", mode="after") @classmethod @@ -248,13 +248,13 @@ def set_logger_level(cls, v: str) -> str: @field_validator("cache", "query_cache", mode="before") @classmethod - def convert_path(cls, v: Any) -> Optional[Path]: + def convert_path(cls, v: Any) -> Path | None: """Convert string paths to Path objects.""" if v is None: return None return Path(v) if not isinstance(v, Path) else v - def get_store_spec(self, store: str) -> Dict[str, Any]: + def get_store_spec(self, store: str) -> dict[str, Any]: """ Get configuration for an external store. @@ -279,11 +279,11 @@ def get_store_spec(self, store: str) -> Dict[str, Any]: raise DataJointError(f'Missing or invalid protocol in config.stores["{store}"]') # Define required and allowed keys by protocol - required_keys: Dict[str, Tuple[str, ...]] = { + required_keys: dict[str, tuple[str, ...]] = { "file": ("protocol", "location"), "s3": ("protocol", "endpoint", "bucket", "access_key", "secret_key", "location"), } - allowed_keys: Dict[str, Tuple[str, ...]] = { + allowed_keys: dict[str, tuple[str, ...]] = { "file": ("protocol", "location", "subfolding", "stage"), "s3": ( "protocol", @@ -311,7 +311,7 @@ def get_store_spec(self, store: str) -> Dict[str, Any]: return spec - def load(self, filename: Union[str, Path]) -> None: + def load(self, filename: str | Path) -> None: """ Load settings from a JSON file. @@ -330,7 +330,7 @@ def load(self, filename: Union[str, Path]) -> None: self._update_from_flat_dict(data) self._config_path = filepath - def _update_from_flat_dict(self, data: Dict[str, Any]) -> None: + def _update_from_flat_dict(self, data: dict[str, Any]) -> None: """Update settings from a flat dict with dot notation keys.""" for key, value in data.items(): parts = key.split(".") diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 9cd63b9e0..a8a52c3e0 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -8,7 +8,6 @@ import re import uuid from pathlib import Path -from typing import Union import numpy as np import pandas @@ -430,7 +429,7 @@ def delete_quick(self, get_count=False): def delete( self, transaction: bool = True, - safemode: Union[bool, None] = None, + safemode: bool | None = None, force_parts: bool = False, force_masters: bool = False, ) -> int: From 83fc7bd5cbc0121cb1bacb032f5949d2ffa5d8b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 20:02:36 +0000 Subject: [PATCH 043/219] Update documentation and CI for Python 3.10+ and MySQL 8.0+ Documentation updates: - docs/src/client/install.md: Update Python requirement from 3.4+ to 3.10+ - docs/src/quick-start.md: Update Python requirement from 3.8+ to 3.10+ - docs/src/quick-start.md: Update MySQL doc link from 5.7 to 8.0 - docs/src/develop.md: Update Python requirement from 3.9+ to 3.10+ - docs/src/develop.md: Fix version.py path (datajoint/ -> src/datajoint/) Configuration updates: - docker-compose.yaml: Update default PY_VER from 3.9 to 3.10 - .github/workflows/test.yaml: Remove Python 3.9 and MySQL 5.7 from test matrix - .github/workflows/post_draft_release_published.yaml: Update to Python 3.10 - .github/workflows/post_draft_release_published.yaml: Fix version.py path --- .github/workflows/post_draft_release_published.yaml | 8 ++++---- .github/workflows/test.yaml | 5 +---- docker-compose.yaml | 2 +- docs/src/client/install.md | 6 +++--- docs/src/develop.md | 6 +++--- docs/src/quick-start.md | 4 ++-- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/post_draft_release_published.yaml b/.github/workflows/post_draft_release_published.yaml index 20160e62b..f9c3ee62d 100644 --- a/.github/workflows/post_draft_release_published.yaml +++ b/.github/workflows/post_draft_release_published.yaml @@ -23,7 +23,7 @@ jobs: strategy: matrix: include: - - py_ver: "3.9" + - py_ver: "3.10" runs-on: ubuntu-latest env: PY_VER: ${{matrix.py_ver}} @@ -40,14 +40,14 @@ jobs: - name: Update version.py run: | VERSION=$(echo "${{ github.event.release.name }}" | grep -oP '\d+\.\d+\.\d+') - sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" datajoint/version.py - cat datajoint/version.py + sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" src/datajoint/version.py + cat src/datajoint/version.py # Commit the changes BRANCH_NAME="update-version-$VERSION" git switch -c $BRANCH_NAME git config --global user.name "github-actions" git config --global user.email "github-actions@github.com" - git add datajoint/version.py + git add src/datajoint/version.py git commit -m "Update version.py to $VERSION" echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV - name: Update README.md badge diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b60040738..6267cd6f1 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,11 +21,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - py_ver: ["3.9", "3.10", "3.11", "3.12", "3.13"] + py_ver: ["3.10", "3.11", "3.12", "3.13"] mysql_ver: ["8.0"] - include: - - py_ver: "3.9" - mysql_ver: "5.7" steps: - uses: actions/checkout@v4 - name: Set up Python ${{matrix.py_ver}} diff --git a/docker-compose.yaml b/docker-compose.yaml index 4c470c3f8..56486dbb6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -41,7 +41,7 @@ services: context: . dockerfile: Dockerfile args: - PY_VER: ${PY_VER:-3.9} + PY_VER: ${PY_VER:-3.10} HOST_UID: ${HOST_UID:-1000} depends_on: db: diff --git a/docs/src/client/install.md b/docs/src/client/install.md index d9684f302..18e6b79f4 100644 --- a/docs/src/client/install.md +++ b/docs/src/client/install.md @@ -1,6 +1,6 @@ # Install and Connect -DataJoint is implemented for Python 3.4+. +DataJoint is implemented for Python 3.10+. You may install it from [PyPI](https://pypi.python.org/pypi/datajoint): ```bash @@ -25,7 +25,7 @@ to connect to DataJoint pipelines. Quick install steps for advanced users are as follows: -- Install latest Python 3.x and ensure it is in `PATH` (3.6.3 current at time of writing) +- Install latest Python 3.x and ensure it is in `PATH` (3.10+ required) ```bash pip install datajoint ``` @@ -46,7 +46,7 @@ Python for Windows is available from: https://www.python.org/downloads/windows -The latest 64 bit 3.x version, currently 3.6.3, is available from the [Python site](https://www.python.org/ftp/python/3.6.3/python-3.6.3-amd64.exe). +The latest 64 bit 3.x version (3.10 or later required) is available from the [Python site](https://www.python.org/downloads/windows/). From here run the installer to install Python. diff --git a/docs/src/develop.md b/docs/src/develop.md index bc636cc20..a4a1fc534 100644 --- a/docs/src/develop.md +++ b/docs/src/develop.md @@ -43,7 +43,7 @@ git clone https://github.com/datajoint/datajoint-python.git ### With Virtual Environment ```bash -# Check if you have Python 3.9 or higher, if not please upgrade +# Check if you have Python 3.10 or higher, if not please upgrade python --version # Create a virtual environment with venv python -m venv .venv @@ -51,7 +51,7 @@ source .venv/bin/activate pip install -e .[dev] # Or create a virtual environment with conda -conda create -n dj python=3.13 # any 3.9+ is fine +conda create -n dj python=3.13 # any 3.10+ is fine conda activate dj pip install -e .[dev] ``` @@ -81,7 +81,7 @@ Here are some options that provide a great developer experience: - Ensure you have [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - Ensure you have [Docker](https://docs.docker.com/get-docker/) - `git clone` the codebase repository and open it in VSCode - - Issue the following command in the terminal to build and run the Docker container: `HOST_UID=$(id -u) PY_VER=3.11 DJ_VERSION=$(grep -oP '\d+\.\d+\.\d+' datajoint/version.py) docker compose --profile test run --rm -it djtest -- sh -c 'pip install -qe ".[dev]" && bash'` + - Issue the following command in the terminal to build and run the Docker container: `HOST_UID=$(id -u) PY_VER=3.11 DJ_VERSION=$(grep -oP '\d+\.\d+\.\d+' src/datajoint/version.py) docker compose --profile test run --rm -it djtest -- sh -c 'pip install -qe ".[dev]" && bash'` - Issue the following command in the terminal to stop the Docker compose stack: `docker compose --profile test down` [Back to top](#table-of-contents) diff --git a/docs/src/quick-start.md b/docs/src/quick-start.md index d52dd50f8..17f783405 100644 --- a/docs/src/quick-start.md +++ b/docs/src/quick-start.md @@ -12,7 +12,7 @@ Advanced users can install DataJoint locally. Please see the installation instru ## Installation First, please [install Python](https://www.python.org/downloads/) version -3.8 or later. +3.10 or later. Next, please install DataJoint via one of the following: @@ -413,7 +413,7 @@ data = query.fetch(order_by='`select` desc') ``` The `order_by` value is eventually passed to the `ORDER BY` -[clause](https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html). +[clause](https://dev.mysql.com/doc/refman/8.0/en/order-by-optimization.html). ### Limiting results From 4518b3623b82398410faf45288305339f51aea9b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 20:49:00 +0000 Subject: [PATCH 044/219] Add initial specification for file column type Draft specification document for the new `file@store` column type that stores files with JSON metadata. Includes syntax, storage format, insert/fetch behavior, and comparison with existing attachment types. --- docs/src/design/tables/file-type-spec.md | 190 +++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 docs/src/design/tables/file-type-spec.md diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md new file mode 100644 index 000000000..0851c8de2 --- /dev/null +++ b/docs/src/design/tables/file-type-spec.md @@ -0,0 +1,190 @@ +# File Column Type Specification + +## Overview + +The `file` type is a new DataJoint column data type that provides managed file storage with metadata tracking. Unlike existing attachment types, `file` stores structured metadata as JSON while managing file storage in a configurable location. + +## Syntax + +```python +@schema +class MyTable(dj.Manual): + definition = """ + id : int + --- + data_file : file@store # managed file with metadata + """ +``` + +## Database Storage + +The `file` type is stored as a `JSON` column in MySQL. The JSON structure contains: + +```json +{ + "path": "relative/path/to/file.ext", + "size": 12345, + "hash": "sha256:abcdef1234...", + "original_name": "original_filename.ext", + "timestamp": "2025-01-15T10:30:00Z", + "mime_type": "application/octet-stream" +} +``` + +### JSON Schema + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `path` | string | Yes | Relative path within the store | +| `size` | integer | Yes | File size in bytes | +| `hash` | string | Yes | Content hash with algorithm prefix | +| `original_name` | string | Yes | Original filename at insert time | +| `timestamp` | string | Yes | ISO 8601 upload timestamp | +| `mime_type` | string | No | MIME type (auto-detected or provided) | + +## Insert Behavior + +At insert time, the `file` attribute accepts: + +1. **File path (string or Path)**: Path to an existing file +2. **Stream object**: File-like object with `read()` method +3. **Tuple of (name, stream)**: Stream with explicit filename + +### Insert Flow + +```python +# From file path +table.insert1({"id": 1, "data_file": "/path/to/file.dat"}) +table.insert1({"id": 2, "data_file": Path("/path/to/file.dat")}) + +# From stream +with open("/path/to/file.dat", "rb") as f: + table.insert1({"id": 3, "data_file": f}) + +# From stream with explicit name +with open("/path/to/file.dat", "rb") as f: + table.insert1({"id": 4, "data_file": ("custom_name.dat", f)}) +``` + +### Processing Steps + +1. Read file content (from path or stream) +2. Compute content hash (SHA-256) +3. Generate storage path using hash-based subfolding +4. Copy file to target location in store +5. Build JSON metadata structure +6. Store JSON in database column + +## Fetch Behavior + +On fetch, the `file` type returns a `FileRef` object (or configurable to return the path string directly). + +```python +# Fetch returns FileRef object +record = table.fetch1() +file_ref = record["data_file"] + +# Access metadata +print(file_ref.path) # Full path to file +print(file_ref.size) # File size +print(file_ref.hash) # Content hash +print(file_ref.original_name) # Original filename + +# Read content +content = file_ref.read() # Returns bytes + +# Get as path +path = file_ref.as_path() # Returns Path object +``` + +### Fetch Options + +```python +# Return path strings instead of FileRef objects +records = table.fetch(download_path="/local/path", format="path") + +# Return raw JSON metadata +records = table.fetch(format="metadata") +``` + +## Store Configuration + +The `file` type uses the existing external store infrastructure: + +```python +dj.config["stores"] = { + "raw": { + "protocol": "file", + "location": "/data/raw-files", + "subfolding": (2, 2), # Hash-based directory structure + }, + "s3store": { + "protocol": "s3", + "endpoint": "s3.amazonaws.com", + "bucket": "my-bucket", + "location": "datajoint-files", + "access_key": "...", + "secret_key": "...", + } +} +``` + +## Comparison with Existing Types + +| Feature | `attach` | `filepath` | `file` | +|---------|----------|------------|--------| +| Storage | External store | External store | External store | +| DB Column | binary(16) UUID | binary(16) UUID | JSON | +| Metadata | Limited | Path + hash | Full structured | +| Deduplication | By content | By path | By content | +| Fetch returns | Downloaded path | Staged path | FileRef object | +| Track history | No | Via hash | Yes (in JSON) | + +## Implementation Components + +### 1. Type Declaration (`declare.py`) + +- Add `FILE` pattern: `file@(?P[a-z][\-\w]*)$` +- Add to `SPECIAL_TYPES` +- Substitute to `JSON` type in database + +### 2. Insert Processing (`table.py`) + +- New `__process_file_attribute()` method +- Handle file path, stream, and (name, stream) inputs +- Copy to store and build metadata JSON + +### 3. Fetch Processing (`fetch.py`) + +- New `FileRef` class for return values +- Optional download/staging behavior +- Metadata access interface + +### 4. Heading Support (`heading.py`) + +- Track `is_file` attribute flag +- Store detection from comment + +## Error Handling + +| Scenario | Behavior | +|----------|----------| +| File not found | Raise `DataJointError` at insert | +| Stream not readable | Raise `DataJointError` at insert | +| Store not configured | Raise `DataJointError` at insert | +| File missing on fetch | Raise `DataJointError` with metadata | +| Hash mismatch on fetch | Warning + option to re-download | + +## Migration Considerations + +- No migration needed - new type, new tables only +- Existing `attach@store` and `filepath@store` unchanged +- Can coexist in same schema + +## Future Extensions + +- [ ] Compression options (gzip, lz4) +- [ ] Encryption at rest +- [ ] Versioning support +- [ ] Lazy loading / streaming fetch +- [ ] Checksum verification options From ba3c66b4b9bed1adc1a2cb7a089d066be8ad0263 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:11:59 +0000 Subject: [PATCH 045/219] Revise file type spec: unified storage backend with fsspec - Single storage backend per pipeline (no @store suffix) - Use fsspec for multi-backend support (local, S3, GCS, Azure) - Configuration via datajoint.toml at project level - Configurable partition patterns based on primary key attributes - Hierarchical project structure with tables/ and objects/ dirs --- docs/src/design/tables/file-type-spec.md | 309 +++++++++++++++-------- 1 file changed, 209 insertions(+), 100 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 0851c8de2..5a45d6bc1 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -2,30 +2,116 @@ ## Overview -The `file` type is a new DataJoint column data type that provides managed file storage with metadata tracking. Unlike existing attachment types, `file` stores structured metadata as JSON while managing file storage in a configurable location. +The `file` type introduces a new paradigm for managed file storage in DataJoint. Unlike existing `attach@store` and `filepath@store` types that reference named stores, the `file` type uses a **unified storage backend** that is tightly coupled with the schema and configured at the pipeline level. + +## Storage Architecture + +### Single Storage Backend Per Pipeline + +Each DataJoint pipeline has **one** associated storage backend configured in `datajoint.toml`. DataJoint fully controls the path structure within this backend. + +### Supported Backends + +DataJoint uses **[`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/)** to ensure compatibility across multiple storage backends: + +- **Local storage** – POSIX-compliant file systems (e.g., NFS, SMB) +- **Cloud-based object storage** – Amazon S3, Google Cloud Storage, Azure Blob, MinIO +- **Hybrid storage** – Combining local and cloud storage for flexibility + +## Project Structure + +A DataJoint project creates a structured hierarchical storage pattern: + +``` +📁 project_name/ +├── datajoint.toml +├── 📁 schema_name1/ +├── 📁 schema_name2/ +├── 📁 schema_name3/ +│ ├── schema.py +│ ├── 📁 tables/ +│ │ ├── table1/key1-value1.parquet +│ │ ├── table2/key2-value2.parquet +│ │ ... +│ ├── 📁 objects/ +│ │ ├── table1-field1/key3-value3.zarr +│ │ ├── table1-field2/key3-value3.gif +│ │ ... +``` + +### Object Storage Keys + +When using cloud object storage: + +``` +s3://bucket/project_name/schema_name3/objects/table1/key1-value1.parquet +s3://bucket/project_name/schema_name3/objects/table1-field1/key3-value3.zarr +``` + +## Configuration + +### `datajoint.toml` Structure + +```toml +[project] +name = "my_project" + +[storage] +backend = "s3" # or "file", "gcs", "azure" +bucket = "my-bucket" +# For local: path = "/data/my_project" + +[storage.credentials] +# Backend-specific credentials (or reference to secrets manager) + +[object_storage] +partition_pattern = "subject{subject_id}/session{session_id}" +``` + +### Partition Pattern + +The organizational structure of stored objects is configurable, allowing partitioning based on **primary key attributes**. + +```toml +[object_storage] +partition_pattern = "subject{subject_id}/session{session_id}" +``` + +Placeholders `{subject_id}` and `{session_id}` are dynamically replaced with actual primary key values. + +**Example with partitioning:** + +``` +s3://my-bucket/project_name/subject123/session45/schema_name3/objects/table1/key1-value1/image1.tiff +s3://my-bucket/project_name/subject123/session45/schema_name3/objects/table2/key2-value2/movie2.zarr +``` ## Syntax ```python @schema -class MyTable(dj.Manual): +class Recording(dj.Manual): definition = """ - id : int + subject_id : int + session_id : int --- - data_file : file@store # managed file with metadata + raw_data : file # managed file storage + processed : file # another file attribute """ ``` +Note: No `@store` suffix needed - storage is determined by pipeline configuration. + ## Database Storage -The `file` type is stored as a `JSON` column in MySQL. The JSON structure contains: +The `file` type is stored as a `JSON` column in MySQL containing: ```json { - "path": "relative/path/to/file.ext", + "path": "subject123/session45/schema_name/objects/Recording-raw_data/...", "size": 12345, "hash": "sha256:abcdef1234...", - "original_name": "original_filename.ext", + "original_name": "recording.dat", "timestamp": "2025-01-15T10:30:00Z", "mime_type": "application/octet-stream" } @@ -35,156 +121,179 @@ The `file` type is stored as a `JSON` column in MySQL. The JSON structure contai | Field | Type | Required | Description | |-------|------|----------|-------------| -| `path` | string | Yes | Relative path within the store | +| `path` | string | Yes | Full path/key within storage backend | | `size` | integer | Yes | File size in bytes | | `hash` | string | Yes | Content hash with algorithm prefix | | `original_name` | string | Yes | Original filename at insert time | | `timestamp` | string | Yes | ISO 8601 upload timestamp | | `mime_type` | string | No | MIME type (auto-detected or provided) | +## Path Generation + +DataJoint generates storage paths using: + +1. **Project name** - from configuration +2. **Partition values** - from primary key (if configured) +3. **Schema name** - from the table's schema +4. **Object directory** - `objects/` +5. **Table-field identifier** - `{table_name}-{field_name}/` +6. **Key identifier** - derived from primary key values +7. **Original filename** - preserved from insert + +Example path construction: + +``` +{project}/{partition}/{schema}/objects/{table}-{field}/{key_hash}/{original_name} +``` + ## Insert Behavior At insert time, the `file` attribute accepts: -1. **File path (string or Path)**: Path to an existing file +1. **File path** (string or `Path`): Path to an existing file 2. **Stream object**: File-like object with `read()` method 3. **Tuple of (name, stream)**: Stream with explicit filename -### Insert Flow - ```python # From file path -table.insert1({"id": 1, "data_file": "/path/to/file.dat"}) -table.insert1({"id": 2, "data_file": Path("/path/to/file.dat")}) - -# From stream -with open("/path/to/file.dat", "rb") as f: - table.insert1({"id": 3, "data_file": f}) +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "/local/path/to/recording.dat" +}) # From stream with explicit name -with open("/path/to/file.dat", "rb") as f: - table.insert1({"id": 4, "data_file": ("custom_name.dat", f)}) +with open("/local/path/data.bin", "rb") as f: + Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": ("custom_name.dat", f) + }) ``` -### Processing Steps +### Insert Processing Steps -1. Read file content (from path or stream) -2. Compute content hash (SHA-256) -3. Generate storage path using hash-based subfolding -4. Copy file to target location in store -5. Build JSON metadata structure -6. Store JSON in database column +1. Resolve storage backend from schema's pipeline configuration +2. Read file content (from path or stream) +3. Compute content hash (SHA-256) +4. Generate storage path using partition pattern and primary key +5. Upload file to storage backend via `fsspec` +6. Build JSON metadata structure +7. Store JSON in database column ## Fetch Behavior -On fetch, the `file` type returns a `FileRef` object (or configurable to return the path string directly). +On fetch, the `file` type returns a `FileRef` object: ```python -# Fetch returns FileRef object -record = table.fetch1() -file_ref = record["data_file"] +record = Recording.fetch1() +file_ref = record["raw_data"] # Access metadata -print(file_ref.path) # Full path to file -print(file_ref.size) # File size +print(file_ref.path) # Full storage path +print(file_ref.size) # File size in bytes print(file_ref.hash) # Content hash print(file_ref.original_name) # Original filename -# Read content +# Read content directly (streams from backend) content = file_ref.read() # Returns bytes -# Get as path -path = file_ref.as_path() # Returns Path object +# Download to local path +local_path = file_ref.download("/local/destination/") + +# Open as fsspec file object +with file_ref.open() as f: + data = f.read() ``` -### Fetch Options +## Implementation Components -```python -# Return path strings instead of FileRef objects -records = table.fetch(download_path="/local/path", format="path") +### 1. Storage Backend (`storage.py` - new module) -# Return raw JSON metadata -records = table.fetch(format="metadata") -``` +- `StorageBackend` class wrapping `fsspec` +- Methods: `upload()`, `download()`, `open()`, `exists()`, `delete()` +- Path generation with partition support +- Configuration loading from `datajoint.toml` -## Store Configuration +### 2. Type Declaration (`declare.py`) -The `file` type uses the existing external store infrastructure: +- Add `FILE` pattern: `file$` +- Add to `SPECIAL_TYPES` +- Substitute to `JSON` type in database -```python -dj.config["stores"] = { - "raw": { - "protocol": "file", - "location": "/data/raw-files", - "subfolding": (2, 2), # Hash-based directory structure - }, - "s3store": { - "protocol": "s3", - "endpoint": "s3.amazonaws.com", - "bucket": "my-bucket", - "location": "datajoint-files", - "access_key": "...", - "secret_key": "...", - } -} -``` +### 3. Schema Integration (`schemas.py`) -## Comparison with Existing Types +- Associate storage backend with schema +- Load configuration on schema creation -| Feature | `attach` | `filepath` | `file` | -|---------|----------|------------|--------| -| Storage | External store | External store | External store | -| DB Column | binary(16) UUID | binary(16) UUID | JSON | -| Metadata | Limited | Path + hash | Full structured | -| Deduplication | By content | By path | By content | -| Fetch returns | Downloaded path | Staged path | FileRef object | -| Track history | No | Via hash | Yes (in JSON) | +### 4. Insert Processing (`table.py`) -## Implementation Components +- New `__process_file_attribute()` method +- Path generation using primary key and partition pattern +- Upload via storage backend -### 1. Type Declaration (`declare.py`) +### 5. Fetch Processing (`fetch.py`) -- Add `FILE` pattern: `file@(?P[a-z][\-\w]*)$` -- Add to `SPECIAL_TYPES` -- Substitute to `JSON` type in database +- New `FileRef` class +- Lazy loading from storage backend +- Metadata access interface -### 2. Insert Processing (`table.py`) +### 6. FileRef Class (`fileref.py` - new module) -- New `__process_file_attribute()` method -- Handle file path, stream, and (name, stream) inputs -- Copy to store and build metadata JSON +```python +class FileRef: + """Reference to a file stored in the pipeline's storage backend.""" + + path: str + size: int + hash: str + original_name: str + timestamp: datetime + mime_type: str | None + + def read(self) -> bytes: ... + def open(self, mode="rb") -> IO: ... + def download(self, destination: Path) -> Path: ... + def exists(self) -> bool: ... +``` -### 3. Fetch Processing (`fetch.py`) +## Dependencies -- New `FileRef` class for return values -- Optional download/staging behavior -- Metadata access interface +New dependency: `fsspec` with optional backend-specific packages: -### 4. Heading Support (`heading.py`) +```toml +[project.dependencies] +fsspec = ">=2023.1.0" -- Track `is_file` attribute flag -- Store detection from comment +[project.optional-dependencies] +s3 = ["s3fs"] +gcs = ["gcsfs"] +azure = ["adlfs"] +``` -## Error Handling +## Comparison with Existing Types -| Scenario | Behavior | -|----------|----------| -| File not found | Raise `DataJointError` at insert | -| Stream not readable | Raise `DataJointError` at insert | -| Store not configured | Raise `DataJointError` at insert | -| File missing on fetch | Raise `DataJointError` with metadata | -| Hash mismatch on fetch | Warning + option to re-download | +| Feature | `attach@store` | `filepath@store` | `file` | +|---------|----------------|------------------|--------| +| Store config | Per-attribute | Per-attribute | Per-pipeline | +| Path control | DataJoint | User-managed | DataJoint | +| DB column | binary(16) UUID | binary(16) UUID | JSON | +| Backend | File/S3 | File/S3 | fsspec (any) | +| Partitioning | Hash-based | User path | Configurable | +| Metadata | External table | External table | Inline JSON | -## Migration Considerations +## Migration Path -- No migration needed - new type, new tables only -- Existing `attach@store` and `filepath@store` unchanged -- Can coexist in same schema +- Existing `attach@store` and `filepath@store` remain unchanged +- `file` type is additive - new tables only +- Future: Migration utilities to convert existing external storage ## Future Extensions -- [ ] Compression options (gzip, lz4) +- [ ] Directory/folder support (store entire directories) +- [ ] Compression options (gzip, lz4, zstd) - [ ] Encryption at rest - [ ] Versioning support -- [ ] Lazy loading / streaming fetch +- [ ] Streaming upload for large files - [ ] Checksum verification options +- [ ] Cache layer for frequently accessed files From 965a30f97d5d18723be4e34c2daedc312c2d6930 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:15:51 +0000 Subject: [PATCH 046/219] Update file type spec to use existing datajoint.json settings - Use datajoint.json instead of datajoint.toml - Add ObjectStorageSettings class spec for settings.py - Support DJ_OBJECT_STORAGE_* environment variables - Support .secrets/ directory for credentials - Partition pattern is per-pipeline (one per settings file) - No deduplication - each record owns its file --- docs/src/design/tables/file-type-spec.md | 167 +++++++++++++++++------ 1 file changed, 126 insertions(+), 41 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 5a45d6bc1..6c8b4e2f3 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -8,7 +8,7 @@ The `file` type introduces a new paradigm for managed file storage in DataJoint. ### Single Storage Backend Per Pipeline -Each DataJoint pipeline has **one** associated storage backend configured in `datajoint.toml`. DataJoint fully controls the path structure within this backend. +Each DataJoint pipeline has **one** associated storage backend configured in `datajoint.json`. DataJoint fully controls the path structure within this backend. ### Supported Backends @@ -16,7 +16,6 @@ DataJoint uses **[`fsspec`](https://filesystem-spec.readthedocs.io/en/latest/)** - **Local storage** – POSIX-compliant file systems (e.g., NFS, SMB) - **Cloud-based object storage** – Amazon S3, Google Cloud Storage, Azure Blob, MinIO -- **Hybrid storage** – Combining local and cloud storage for flexibility ## Project Structure @@ -24,7 +23,7 @@ A DataJoint project creates a structured hierarchical storage pattern: ``` 📁 project_name/ -├── datajoint.toml +├── datajoint.json ├── 📁 schema_name1/ ├── 📁 schema_name2/ ├── 📁 schema_name3/ @@ -50,42 +49,84 @@ s3://bucket/project_name/schema_name3/objects/table1-field1/key3-value3.zarr ## Configuration -### `datajoint.toml` Structure +### Settings Structure -```toml -[project] -name = "my_project" +Object storage is configured in `datajoint.json` using the existing settings system: -[storage] -backend = "s3" # or "file", "gcs", "azure" -bucket = "my-bucket" -# For local: path = "/data/my_project" +```json +{ + "database.host": "localhost", + "database.user": "datajoint", + + "object_storage.protocol": "s3", + "object_storage.endpoint": "s3.amazonaws.com", + "object_storage.bucket": "my-bucket", + "object_storage.location": "my_project", + "object_storage.partition_pattern": "subject{subject_id}/session{session_id}" +} +``` -[storage.credentials] -# Backend-specific credentials (or reference to secrets manager) +For local filesystem storage: -[object_storage] -partition_pattern = "subject{subject_id}/session{session_id}" +```json +{ + "object_storage.protocol": "file", + "object_storage.location": "/data/my_project", + "object_storage.partition_pattern": "subject{subject_id}/session{session_id}" +} ``` -### Partition Pattern +### Settings Schema -The organizational structure of stored objects is configurable, allowing partitioning based on **primary key attributes**. +| Setting | Type | Required | Description | +|---------|------|----------|-------------| +| `object_storage.protocol` | string | Yes | Storage backend: `file`, `s3`, `gcs`, `azure` | +| `object_storage.location` | string | Yes | Base path or bucket prefix | +| `object_storage.bucket` | string | For cloud | Bucket name (S3, GCS, Azure) | +| `object_storage.endpoint` | string | For S3 | S3 endpoint URL | +| `object_storage.partition_pattern` | string | No | Path pattern with `{attribute}` placeholders | +| `object_storage.access_key` | string | For cloud | Access key (can use secrets file) | +| `object_storage.secret_key` | string | For cloud | Secret key (can use secrets file) | -```toml -[object_storage] -partition_pattern = "subject{subject_id}/session{session_id}" +### Environment Variables + +Settings can be overridden via environment variables: + +```bash +DJ_OBJECT_STORAGE_PROTOCOL=s3 +DJ_OBJECT_STORAGE_BUCKET=my-bucket +DJ_OBJECT_STORAGE_LOCATION=my_project +DJ_OBJECT_STORAGE_PARTITION_PATTERN="subject{subject_id}/session{session_id}" ``` -Placeholders `{subject_id}` and `{session_id}` are dynamically replaced with actual primary key values. +### Secrets + +Credentials can be stored in the `.secrets/` directory: + +``` +.secrets/ +├── object_storage.access_key +└── object_storage.secret_key +``` + +### Partition Pattern + +The partition pattern is configured **per pipeline** (one per settings file). Placeholders use `{attribute_name}` syntax and are replaced with primary key values. + +```json +{ + "object_storage.partition_pattern": "subject{subject_id}/session{session_id}" +} +``` **Example with partitioning:** ``` -s3://my-bucket/project_name/subject123/session45/schema_name3/objects/table1/key1-value1/image1.tiff -s3://my-bucket/project_name/subject123/session45/schema_name3/objects/table2/key2-value2/movie2.zarr +s3://my-bucket/my_project/subject123/session45/schema_name/objects/Recording-raw_data/recording.dat ``` +If no partition pattern is specified, files are organized directly under `{location}/{schema}/objects/`. + ## Syntax ```python @@ -108,7 +149,7 @@ The `file` type is stored as a `JSON` column in MySQL containing: ```json { - "path": "subject123/session45/schema_name/objects/Recording-raw_data/...", + "path": "subject123/session45/schema_name/objects/Recording-raw_data/recording.dat", "size": 12345, "hash": "sha256:abcdef1234...", "original_name": "recording.dat", @@ -132,20 +173,27 @@ The `file` type is stored as a `JSON` column in MySQL containing: DataJoint generates storage paths using: -1. **Project name** - from configuration -2. **Partition values** - from primary key (if configured) +1. **Location** - from configuration (`object_storage.location`) +2. **Partition values** - from primary key (if `partition_pattern` configured) 3. **Schema name** - from the table's schema 4. **Object directory** - `objects/` -5. **Table-field identifier** - `{table_name}-{field_name}/` -6. **Key identifier** - derived from primary key values +5. **Table-field identifier** - `{TableName}-{field_name}/` +6. **Primary key hash** - unique identifier for the record 7. **Original filename** - preserved from insert Example path construction: ``` -{project}/{partition}/{schema}/objects/{table}-{field}/{key_hash}/{original_name} +{location}/{partition}/{schema}/objects/{Table}-{field}/{pk_hash}/{original_name} ``` +### No Deduplication + +Each insert stores a separate copy of the file, even if identical content was previously stored. This ensures: +- Clear 1:1 relationship between records and files +- Simplified delete behavior +- No reference counting complexity + ## Insert Behavior At insert time, the `file` attribute accepts: @@ -173,7 +221,7 @@ with open("/local/path/data.bin", "rb") as f: ### Insert Processing Steps -1. Resolve storage backend from schema's pipeline configuration +1. Resolve storage backend from pipeline configuration 2. Read file content (from path or stream) 3. Compute content hash (SHA-256) 4. Generate storage path using partition pattern and primary key @@ -208,39 +256,68 @@ with file_ref.open() as f: ## Implementation Components -### 1. Storage Backend (`storage.py` - new module) +### 1. Settings Extension (`settings.py`) + +New `ObjectStorageSettings` class: + +```python +class ObjectStorageSettings(BaseSettings): + """Object storage configuration for file columns.""" + + model_config = SettingsConfigDict( + env_prefix="DJ_OBJECT_STORAGE_", + extra="forbid", + validate_assignment=True, + ) + + protocol: Literal["file", "s3", "gcs", "azure"] | None = None + location: str | None = None + bucket: str | None = None + endpoint: str | None = None + partition_pattern: str | None = None + access_key: str | None = None + secret_key: SecretStr | None = None +``` + +Add to main `Config` class: + +```python +object_storage: ObjectStorageSettings = Field(default_factory=ObjectStorageSettings) +``` + +### 2. Storage Backend (`storage.py` - new module) - `StorageBackend` class wrapping `fsspec` - Methods: `upload()`, `download()`, `open()`, `exists()`, `delete()` - Path generation with partition support -- Configuration loading from `datajoint.toml` -### 2. Type Declaration (`declare.py`) +### 3. Type Declaration (`declare.py`) - Add `FILE` pattern: `file$` - Add to `SPECIAL_TYPES` - Substitute to `JSON` type in database -### 3. Schema Integration (`schemas.py`) +### 4. Schema Integration (`schemas.py`) - Associate storage backend with schema -- Load configuration on schema creation +- Validate storage configuration on schema creation -### 4. Insert Processing (`table.py`) +### 5. Insert Processing (`table.py`) - New `__process_file_attribute()` method - Path generation using primary key and partition pattern - Upload via storage backend -### 5. Fetch Processing (`fetch.py`) +### 6. Fetch Processing (`fetch.py`) - New `FileRef` class - Lazy loading from storage backend - Metadata access interface -### 6. FileRef Class (`fileref.py` - new module) +### 7. FileRef Class (`fileref.py` - new module) ```python +@dataclass class FileRef: """Reference to a file stored in the pipeline's storage backend.""" @@ -250,10 +327,11 @@ class FileRef: original_name: str timestamp: datetime mime_type: str | None + _backend: StorageBackend # internal reference def read(self) -> bytes: ... - def open(self, mode="rb") -> IO: ... - def download(self, destination: Path) -> Path: ... + def open(self, mode: str = "rb") -> IO: ... + def download(self, destination: Path | str) -> Path: ... def exists(self) -> bool: ... ``` @@ -278,9 +356,16 @@ azure = ["adlfs"] | Store config | Per-attribute | Per-attribute | Per-pipeline | | Path control | DataJoint | User-managed | DataJoint | | DB column | binary(16) UUID | binary(16) UUID | JSON | -| Backend | File/S3 | File/S3 | fsspec (any) | +| Backend | File/S3 only | File/S3 only | fsspec (any) | | Partitioning | Hash-based | User path | Configurable | | Metadata | External table | External table | Inline JSON | +| Deduplication | By content | By path | None | + +## Delete Behavior + +When a record with a `file` attribute is deleted: +- The corresponding file in storage is also deleted +- No reference counting (each record owns its file) ## Migration Path From 667e740ce2e427c776e27121cd8768c41ce417de Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:21:47 +0000 Subject: [PATCH 047/219] Add filename collision avoidance and transaction handling to spec - Random hash suffix for filenames (URL-safe, filename-safe base64) - Configurable hash_length setting (default: 8, range: 4-16) - Upload-first transaction strategy with cleanup on failure - Batch insert atomicity handling - Orphaned file detection/cleanup utilities (future) --- docs/src/design/tables/file-type-spec.md | 90 ++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 6c8b4e2f3..87596d48d 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -85,6 +85,7 @@ For local filesystem storage: | `object_storage.bucket` | string | For cloud | Bucket name (S3, GCS, Azure) | | `object_storage.endpoint` | string | For S3 | S3 endpoint URL | | `object_storage.partition_pattern` | string | No | Path pattern with `{attribute}` placeholders | +| `object_storage.hash_length` | int | No | Random suffix length for filenames (default: 8, range: 4-16) | | `object_storage.access_key` | string | For cloud | Access key (can use secrets file) | | `object_storage.secret_key` | string | For cloud | Secret key (can use secrets file) | @@ -149,7 +150,7 @@ The `file` type is stored as a `JSON` column in MySQL containing: ```json { - "path": "subject123/session45/schema_name/objects/Recording-raw_data/recording.dat", + "path": "subject123/session45/schema_name/objects/Recording-raw_data/recording_Ax7bQ2kM.dat", "size": 12345, "hash": "sha256:abcdef1234...", "original_name": "recording.dat", @@ -178,15 +179,41 @@ DataJoint generates storage paths using: 3. **Schema name** - from the table's schema 4. **Object directory** - `objects/` 5. **Table-field identifier** - `{TableName}-{field_name}/` -6. **Primary key hash** - unique identifier for the record -7. **Original filename** - preserved from insert +6. **Suffixed filename** - original name with random hash suffix Example path construction: ``` -{location}/{partition}/{schema}/objects/{Table}-{field}/{pk_hash}/{original_name} +{location}/{partition}/{schema}/objects/{Table}-{field}/{basename}_{hash}.{ext} ``` +### Filename Collision Avoidance + +To prevent filename collisions, each stored file receives a **random hash suffix** appended to its basename: + +``` +original: recording.dat +stored: recording_Ax7bQ2kM.dat + +original: image.analysis.tiff +stored: image.analysis_pL9nR4wE.tiff +``` + +#### Hash Suffix Specification + +- **Alphabet**: URL-safe and filename-safe Base64 characters: `A-Z`, `a-z`, `0-9`, `-`, `_` +- **Length**: Configurable via `object_storage.hash_length` (default: 8, range: 4-16) +- **Generation**: Cryptographically random using `secrets.token_urlsafe()` + +At 8 characters with 64 possible values per character: 64^8 = 281 trillion combinations. + +#### Rationale + +- Avoids collisions without requiring existence checks +- Preserves original filename for human readability +- URL-safe for web-based access to cloud storage +- Filesystem-safe across all supported platforms + ### No Deduplication Each insert stores a separate copy of the file, even if identical content was previously stored. This ensures: @@ -224,11 +251,63 @@ with open("/local/path/data.bin", "rb") as f: 1. Resolve storage backend from pipeline configuration 2. Read file content (from path or stream) 3. Compute content hash (SHA-256) -4. Generate storage path using partition pattern and primary key +4. Generate storage path with random suffix 5. Upload file to storage backend via `fsspec` 6. Build JSON metadata structure 7. Store JSON in database column +## Transaction Handling + +File uploads and database inserts must be coordinated to maintain consistency. Since storage backends don't support distributed transactions with MySQL, DataJoint uses a **upload-first** strategy with cleanup on failure. + +### Insert Transaction Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Validate input and generate storage path │ +├─────────────────────────────────────────────────────────┤ +│ 2. Upload file to storage backend │ +│ └─ On failure: raise error (nothing to clean up) │ +├─────────────────────────────────────────────────────────┤ +│ 3. Build JSON metadata with storage path │ +├─────────────────────────────────────────────────────────┤ +│ 4. Execute database INSERT │ +│ └─ On failure: delete uploaded file, raise error │ +├─────────────────────────────────────────────────────────┤ +│ 5. Commit database transaction │ +│ └─ On failure: delete uploaded file, raise error │ +└─────────────────────────────────────────────────────────┘ +``` + +### Failure Scenarios + +| Scenario | State Before | Recovery Action | Result | +|----------|--------------|-----------------|--------| +| Upload fails | No file, no record | None needed | Clean failure | +| DB insert fails | File exists, no record | Delete file | Clean failure | +| DB commit fails | File exists, no record | Delete file | Clean failure | +| Cleanup fails | File exists, no record | Log warning | Orphaned file | + +### Orphaned File Handling + +In rare cases (e.g., process crash, network failure during cleanup), orphaned files may remain in storage. These can be identified and cleaned: + +```python +# Future utility method +schema.external_storage.find_orphaned() # List files not referenced in DB +schema.external_storage.cleanup_orphaned() # Delete orphaned files +``` + +### Batch Insert Handling + +For batch inserts with multiple `file` attributes: + +1. Upload all files first (collect paths) +2. Execute batch INSERT with all metadata +3. On any failure: delete all uploaded files from this batch + +This ensures atomicity at the batch level - either all records are inserted with their files, or none are. + ## Fetch Behavior On fetch, the `file` type returns a `FileRef` object: @@ -275,6 +354,7 @@ class ObjectStorageSettings(BaseSettings): bucket: str | None = None endpoint: str | None = None partition_pattern: str | None = None + hash_length: int = Field(default=8, ge=4, le=16) access_key: str | None = None secret_key: SecretStr | None = None ``` From 9d3e1945ede55799250a0f207c4e77f9645909fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:39:00 +0000 Subject: [PATCH 048/219] Major spec revision: files/folders, transactions, fetch handles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key changes: - Support both files and folders - Immutability contract: insert, read, delete only - Deterministic bidirectional path mapping from schema/table/field/PK - Copy-first insert: copy fails → no DB insert attempted - DB-first delete: file delete is best-effort (stale files acceptable) - Fetch returns handle (FileRef), no automatic download - JSON metadata includes is_folder, file_count for folders - FileRef class with folder operations (listdir, walk) --- docs/src/design/tables/file-type-spec.md | 265 +++++++++++++++++------ 1 file changed, 198 insertions(+), 67 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 87596d48d..cf204cf11 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -4,6 +4,17 @@ The `file` type introduces a new paradigm for managed file storage in DataJoint. Unlike existing `attach@store` and `filepath@store` types that reference named stores, the `file` type uses a **unified storage backend** that is tightly coupled with the schema and configured at the pipeline level. +The `file` type supports both **files and folders**. Content is copied to storage at insert time, referenced via handle on fetch, and deleted when the record is deleted. + +### Immutability Contract + +Files stored via the `file` type are **immutable**. Users agree to: +- **Insert**: Copy content to storage (only way to create) +- **Fetch**: Read content via handle (no modification) +- **Delete**: Remove content when record is deleted (only way to remove) + +Users must not directly modify files in the object store. + ## Storage Architecture ### Single Storage Backend Per Pipeline @@ -148,45 +159,98 @@ Note: No `@store` suffix needed - storage is determined by pipeline configuratio The `file` type is stored as a `JSON` column in MySQL containing: +**File example:** ```json { - "path": "subject123/session45/schema_name/objects/Recording-raw_data/recording_Ax7bQ2kM.dat", + "path": "my_schema/objects/Recording/raw_data/subject_id=123/session_id=45/recording_Ax7bQ2kM.dat", "size": 12345, "hash": "sha256:abcdef1234...", "original_name": "recording.dat", + "is_folder": false, "timestamp": "2025-01-15T10:30:00Z", "mime_type": "application/octet-stream" } ``` +**Folder example:** +```json +{ + "path": "my_schema/objects/Recording/raw_data/subject_id=123/session_id=45/data_folder_pL9nR4wE", + "size": 567890, + "hash": "sha256:fedcba9876...", + "original_name": "data_folder", + "is_folder": true, + "timestamp": "2025-01-15T10:30:00Z", + "file_count": 42 +} +``` + ### JSON Schema | Field | Type | Required | Description | |-------|------|----------|-------------| -| `path` | string | Yes | Full path/key within storage backend | -| `size` | integer | Yes | File size in bytes | +| `path` | string | Yes | Full path/key within storage backend (includes token) | +| `size` | integer | Yes | Total size in bytes (sum for folders) | | `hash` | string | Yes | Content hash with algorithm prefix | -| `original_name` | string | Yes | Original filename at insert time | +| `original_name` | string | Yes | Original file/folder name at insert time | +| `is_folder` | boolean | Yes | True if stored content is a directory | | `timestamp` | string | Yes | ISO 8601 upload timestamp | -| `mime_type` | string | No | MIME type (auto-detected or provided) | +| `mime_type` | string | No | MIME type (files only, auto-detected or provided) | +| `file_count` | integer | No | Number of files (folders only) | ## Path Generation -DataJoint generates storage paths using: +Storage paths are **deterministically constructed** from record metadata, enabling bidirectional lookup between database records and stored files. + +### Path Components 1. **Location** - from configuration (`object_storage.location`) -2. **Partition values** - from primary key (if `partition_pattern` configured) -3. **Schema name** - from the table's schema -4. **Object directory** - `objects/` -5. **Table-field identifier** - `{TableName}-{field_name}/` -6. **Suffixed filename** - original name with random hash suffix +2. **Schema name** - from the table's schema +3. **Object directory** - `objects/` +4. **Table name** - the table class name +5. **Field name** - the attribute name +6. **Primary key encoding** - all PK attributes and values +7. **Suffixed filename** - original name with random hash suffix -Example path construction: +### Path Template ``` -{location}/{partition}/{schema}/objects/{Table}-{field}/{basename}_{hash}.{ext} +{location}/{schema}/objects/{Table}/{field}/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../{basename}_{token}.{ext} ``` +### Example + +For a table: +```python +@schema +class Recording(dj.Manual): + definition = """ + subject_id : int + session_id : int + --- + raw_data : file + """ +``` + +Inserting `{"subject_id": 123, "session_id": 45, "raw_data": "/path/to/recording.dat"}` produces: + +``` +my_project/my_schema/objects/Recording/raw_data/subject_id=123/session_id=45/recording_Ax7bQ2kM.dat +``` + +### Deterministic Bidirectional Mapping + +The path structure (excluding the random token) is fully deterministic: +- **Record → File**: Given a record's primary key, construct the path prefix to locate its file +- **File → Record**: Parse the path to extract schema, table, field, and primary key values + +This enables: +- Finding all files for a specific record +- Identifying which record a file belongs to +- Auditing storage against database contents + +The **random token** is stored in the JSON metadata to complete the full path. + ### Filename Collision Avoidance To prevent filename collisions, each stored file receives a **random hash suffix** appended to its basename: @@ -226,8 +290,9 @@ Each insert stores a separate copy of the file, even if identical content was pr At insert time, the `file` attribute accepts: 1. **File path** (string or `Path`): Path to an existing file -2. **Stream object**: File-like object with `read()` method -3. **Tuple of (name, stream)**: Stream with explicit filename +2. **Folder path** (string or `Path`): Path to an existing directory +3. **Stream object**: File-like object with `read()` method +4. **Tuple of (name, stream)**: Stream with explicit filename ```python # From file path @@ -237,6 +302,13 @@ Recording.insert1({ "raw_data": "/local/path/to/recording.dat" }) +# From folder path +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "/local/path/to/data_folder/" +}) + # From stream with explicit name with open("/local/path/data.bin", "rb") as f: Recording.insert1({ @@ -248,89 +320,112 @@ with open("/local/path/data.bin", "rb") as f: ### Insert Processing Steps -1. Resolve storage backend from pipeline configuration -2. Read file content (from path or stream) -3. Compute content hash (SHA-256) -4. Generate storage path with random suffix -5. Upload file to storage backend via `fsspec` +1. Validate input (file/folder exists, stream is readable) +2. Generate deterministic storage path with random token +3. **Copy content to storage backend** via `fsspec` +4. **If copy fails: abort insert** (no database operation attempted) +5. Compute content hash (SHA-256) 6. Build JSON metadata structure -7. Store JSON in database column +7. Execute database INSERT + +### Copy-First Semantics + +The file/folder is copied to storage **before** the database insert is attempted: +- If the copy fails, the insert does not proceed +- If the copy succeeds but the database insert fails, an orphaned file may remain +- Orphaned files are acceptable due to the random token (no collision with future inserts) ## Transaction Handling -File uploads and database inserts must be coordinated to maintain consistency. Since storage backends don't support distributed transactions with MySQL, DataJoint uses a **upload-first** strategy with cleanup on failure. +Since storage backends don't support distributed transactions with MySQL, DataJoint uses a **copy-first** strategy. ### Insert Transaction Flow ``` ┌─────────────────────────────────────────────────────────┐ -│ 1. Validate input and generate storage path │ +│ 1. Validate input and generate storage path with token │ ├─────────────────────────────────────────────────────────┤ -│ 2. Upload file to storage backend │ -│ └─ On failure: raise error (nothing to clean up) │ +│ 2. Copy file/folder to storage backend │ +│ └─ On failure: raise error, INSERT not attempted │ ├─────────────────────────────────────────────────────────┤ -│ 3. Build JSON metadata with storage path │ +│ 3. Compute hash and build JSON metadata │ ├─────────────────────────────────────────────────────────┤ │ 4. Execute database INSERT │ -│ └─ On failure: delete uploaded file, raise error │ +│ └─ On failure: orphaned file remains (acceptable) │ ├─────────────────────────────────────────────────────────┤ │ 5. Commit database transaction │ -│ └─ On failure: delete uploaded file, raise error │ +│ └─ On failure: orphaned file remains (acceptable) │ └─────────────────────────────────────────────────────────┘ ``` ### Failure Scenarios -| Scenario | State Before | Recovery Action | Result | -|----------|--------------|-----------------|--------| -| Upload fails | No file, no record | None needed | Clean failure | -| DB insert fails | File exists, no record | Delete file | Clean failure | -| DB commit fails | File exists, no record | Delete file | Clean failure | -| Cleanup fails | File exists, no record | Log warning | Orphaned file | +| Scenario | Result | Orphaned File? | +|----------|--------|----------------| +| Copy fails | Clean failure, no INSERT | No | +| DB insert fails | Error raised | Yes (acceptable) | +| DB commit fails | Error raised | Yes (acceptable) | + +### Orphaned Files -### Orphaned File Handling +Orphaned files (files in storage without corresponding database records) may accumulate due to: +- Failed database inserts after successful copy +- Process crashes +- Network failures -In rare cases (e.g., process crash, network failure during cleanup), orphaned files may remain in storage. These can be identified and cleaned: +**This is acceptable** because: +- Random tokens prevent collisions with future inserts +- Orphaned files can be identified by comparing storage contents with database records +- Cleanup utilities can remove orphaned files periodically ```python -# Future utility method -schema.external_storage.find_orphaned() # List files not referenced in DB -schema.external_storage.cleanup_orphaned() # Delete orphaned files +# Future utility methods +schema.file_storage.find_orphaned() # List files not referenced in DB +schema.file_storage.cleanup_orphaned() # Delete orphaned files ``` -### Batch Insert Handling - -For batch inserts with multiple `file` attributes: - -1. Upload all files first (collect paths) -2. Execute batch INSERT with all metadata -3. On any failure: delete all uploaded files from this batch - -This ensures atomicity at the batch level - either all records are inserted with their files, or none are. - ## Fetch Behavior -On fetch, the `file` type returns a `FileRef` object: +On fetch, the `file` type returns a **handle** (`FileRef` object) to the stored content. **The file is not copied** - all operations access the storage backend directly. ```python record = Recording.fetch1() file_ref = record["raw_data"] -# Access metadata +# Access metadata (no I/O) print(file_ref.path) # Full storage path print(file_ref.size) # File size in bytes print(file_ref.hash) # Content hash print(file_ref.original_name) # Original filename +print(file_ref.is_folder) # True if stored content is a folder -# Read content directly (streams from backend) -content = file_ref.read() # Returns bytes - -# Download to local path -local_path = file_ref.download("/local/destination/") +# Read content directly from storage backend +content = file_ref.read() # Returns bytes (files only) -# Open as fsspec file object +# Open as fsspec file object (files only) with file_ref.open() as f: data = f.read() + +# List contents (folders only) +contents = file_ref.listdir() # Returns list of relative paths + +# Access specific file within folder +with file_ref.open("subdir/file.dat") as f: + data = f.read() +``` + +### No Automatic Download + +Unlike `attach@store`, the `file` type does **not** automatically download content to a local path. Users access content directly through the `FileRef` handle, which streams from the storage backend. + +For local copies, users explicitly download: + +```python +# Download file to local destination +local_path = file_ref.download("/local/destination/") + +# Download specific file from folder +local_path = file_ref.download("/local/destination/", "subdir/file.dat") ``` ## Implementation Components @@ -399,20 +494,29 @@ object_storage: ObjectStorageSettings = Field(default_factory=ObjectStorageSetti ```python @dataclass class FileRef: - """Reference to a file stored in the pipeline's storage backend.""" + """Handle to a file or folder stored in the pipeline's storage backend.""" path: str size: int hash: str original_name: str + is_folder: bool timestamp: datetime - mime_type: str | None - _backend: StorageBackend # internal reference + mime_type: str | None # files only + file_count: int | None # folders only + _backend: StorageBackend # internal reference + # File operations def read(self) -> bytes: ... - def open(self, mode: str = "rb") -> IO: ... - def download(self, destination: Path | str) -> Path: ... - def exists(self) -> bool: ... + def open(self, subpath: str | None = None, mode: str = "rb") -> IO: ... + + # Folder operations + def listdir(self, subpath: str = "") -> list[str]: ... + def walk(self) -> Iterator[tuple[str, list[str], list[str]]]: ... + + # Common operations + def download(self, destination: Path | str, subpath: str | None = None) -> Path: ... + def exists(self, subpath: str | None = None) -> bool: ... ``` ## Dependencies @@ -444,8 +548,35 @@ azure = ["adlfs"] ## Delete Behavior When a record with a `file` attribute is deleted: -- The corresponding file in storage is also deleted -- No reference counting (each record owns its file) + +1. **Database delete executes first** (within transaction) +2. **File delete is attempted** after successful DB commit +3. **File delete is best-effort** - the delete transaction succeeds even if file deletion fails + +### Delete Transaction Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Execute database DELETE │ +├─────────────────────────────────────────────────────────┤ +│ 2. Commit database transaction │ +│ └─ On failure: rollback, files unchanged │ +├─────────────────────────────────────────────────────────┤ +│ 3. Issue delete command to storage backend │ +│ └─ On failure: log warning, transaction still OK │ +└─────────────────────────────────────────────────────────┘ +``` + +### Stale Files + +If file deletion fails (network error, permissions, etc.), **stale files** may remain in storage. This is acceptable because: +- The database record is already deleted (authoritative source) +- Random tokens prevent any collision with future inserts +- Stale files can be identified and cleaned via orphan detection utilities + +### No Reference Counting + +Each record owns its file exclusively. There is no deduplication or reference counting, simplifying delete logic. ## Migration Path @@ -455,10 +586,10 @@ When a record with a `file` attribute is deleted: ## Future Extensions -- [ ] Directory/folder support (store entire directories) - [ ] Compression options (gzip, lz4, zstd) - [ ] Encryption at rest - [ ] Versioning support - [ ] Streaming upload for large files -- [ ] Checksum verification options +- [ ] Checksum verification on fetch - [ ] Cache layer for frequently accessed files +- [ ] Parallel upload/download for large folders From 93559a4d30fe3286d8e364c5d48682d383765678 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:47:20 +0000 Subject: [PATCH 049/219] Update path structure: field after PK, add partition pattern Path changes: - Field name now comes after all primary key attributes - Groups related files together (all fields for same record in same dir) Partitioning: - partition_pattern config promotes PK attributes to path root - Enables grouping by high-level attributes (subject, experiment) - Example: {subject_id} moves subject to path start for data locality --- docs/src/design/tables/file-type-spec.md | 55 +++++++++++++++++++----- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index cf204cf11..e45d8820e 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -162,7 +162,7 @@ The `file` type is stored as a `JSON` column in MySQL containing: **File example:** ```json { - "path": "my_schema/objects/Recording/raw_data/subject_id=123/session_id=45/recording_Ax7bQ2kM.dat", + "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data/recording_Ax7bQ2kM.dat", "size": 12345, "hash": "sha256:abcdef1234...", "original_name": "recording.dat", @@ -175,7 +175,7 @@ The `file` type is stored as a `JSON` column in MySQL containing: **Folder example:** ```json { - "path": "my_schema/objects/Recording/raw_data/subject_id=123/session_id=45/data_folder_pL9nR4wE", + "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data/data_folder_pL9nR4wE", "size": 567890, "hash": "sha256:fedcba9876...", "original_name": "data_folder", @@ -205,20 +205,43 @@ Storage paths are **deterministically constructed** from record metadata, enabli ### Path Components 1. **Location** - from configuration (`object_storage.location`) -2. **Schema name** - from the table's schema -3. **Object directory** - `objects/` -4. **Table name** - the table class name -5. **Field name** - the attribute name -6. **Primary key encoding** - all PK attributes and values -7. **Suffixed filename** - original name with random hash suffix +2. **Partition attributes** - promoted PK attributes (if `partition_pattern` configured) +3. **Schema name** - from the table's schema +4. **Object directory** - `objects/` +5. **Table name** - the table class name +6. **Primary key encoding** - remaining PK attributes and values +7. **Field name** - the attribute name +8. **Suffixed filename** - original name with random hash suffix ### Path Template +**Without partitioning:** ``` -{location}/{schema}/objects/{Table}/{field}/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../{basename}_{token}.{ext} +{location}/{schema}/objects/{Table}/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../field/{basename}_{token}.{ext} ``` -### Example +**With partitioning:** +``` +{location}/{partition_attr}={val}/.../schema/objects/{Table}/{remaining_pk_attrs}/.../field/{basename}_{token}.{ext} +``` + +### Partitioning + +The **partition pattern** allows promoting certain primary key attributes to the beginning of the path (after `location`). This organizes storage by high-level attributes like subject or experiment, enabling: +- Efficient data locality for related records +- Easier manual browsing of storage +- Potential for storage tiering by partition + +**Configuration:** +```json +{ + "object_storage.partition_pattern": "{subject_id}/{experiment_id}" +} +``` + +Partition attributes are extracted from the primary key and placed at the path root. Remaining PK attributes appear in their normal position. + +### Example Without Partitioning For a table: ```python @@ -235,9 +258,19 @@ class Recording(dj.Manual): Inserting `{"subject_id": 123, "session_id": 45, "raw_data": "/path/to/recording.dat"}` produces: ``` -my_project/my_schema/objects/Recording/raw_data/subject_id=123/session_id=45/recording_Ax7bQ2kM.dat +my_project/my_schema/objects/Recording/subject_id=123/session_id=45/raw_data/recording_Ax7bQ2kM.dat +``` + +### Example With Partitioning + +With `partition_pattern = "{subject_id}"`: + +``` +my_project/subject_id=123/my_schema/objects/Recording/session_id=45/raw_data/recording_Ax7bQ2kM.dat ``` +The `subject_id` is promoted to the path root, grouping all files for subject 123 together regardless of schema or table. + ### Deterministic Bidirectional Mapping The path structure (excluding the random token) is fully deterministic: From dc1c8995c241ac0789a377d90746a1336ed9faf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:52:13 +0000 Subject: [PATCH 050/219] Add PK value encoding rules for paths - Keep = sign in paths (Hive convention, widely supported) - Simple types used directly: integers, dates, timestamps, strings - Conversion to path-safe strings only when necessary: - Path-unsafe characters (/, \) get URL-encoded - Long strings truncated with hash suffix - Binary/complex types hashed --- docs/src/design/tables/file-type-spec.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index e45d8820e..a23f49f1e 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -284,6 +284,30 @@ This enables: The **random token** is stored in the JSON metadata to complete the full path. +### Primary Key Value Encoding + +Primary key values are encoded directly in paths when they are simple, path-safe types: +- **Integers**: Used directly (`subject_id=123`) +- **Dates**: ISO format (`session_date=2025-01-15`) +- **Timestamps**: ISO format with safe separators (`created=2025-01-15T10-30-00`) +- **Simple strings**: Used directly if path-safe (`experiment=baseline`) + +**Conversion to path-safe strings** is applied only when necessary: +- Strings containing `/`, `\`, or other path-unsafe characters +- Very long strings (truncated with hash suffix) +- Binary or complex types (hashed) + +```python +# Direct encoding (no conversion needed) +subject_id=123 +session_date=2025-01-15 +trial_type=control + +# Converted encoding (path-unsafe characters) +filename=my%2Ffile.dat # "/" encoded +description=a1b2c3d4_abc123 # long string truncated + hash +``` + ### Filename Collision Avoidance To prevent filename collisions, each stored file receives a **random hash suffix** appended to its basename: From 5f27b75f071902a7d47bd1985ac3a8ff9fc729b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:55:00 +0000 Subject: [PATCH 051/219] Clarify orphan cleanup as separate maintenance procedure - Orphan cleanup must run during maintenance windows - Uses transactions/locking to avoid race conditions - Grace period excludes recently uploaded files (in-flight inserts) - Dry-run mode for previewing deletions --- docs/src/design/tables/file-type-spec.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index a23f49f1e..c14deb2e0 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -433,14 +433,24 @@ Orphaned files (files in storage without corresponding database records) may acc **This is acceptable** because: - Random tokens prevent collisions with future inserts - Orphaned files can be identified by comparing storage contents with database records -- Cleanup utilities can remove orphaned files periodically +- A separate cleanup procedure removes orphaned files during maintenance + +### Orphan Cleanup Procedure + +Orphan cleanup is a **separate maintenance operation** that must be performed during maintenance windows to avoid race conditions with concurrent inserts. ```python -# Future utility methods +# Maintenance utility methods schema.file_storage.find_orphaned() # List files not referenced in DB schema.file_storage.cleanup_orphaned() # Delete orphaned files ``` +**Important considerations:** +- Should be run during low-activity periods +- Uses transactions or locking to avoid race conditions with concurrent inserts +- Files recently uploaded (within a grace period) are excluded to handle in-flight inserts +- Provides dry-run mode to preview deletions before execution + ## Fetch Behavior On fetch, the `file` type returns a **handle** (`FileRef` object) to the stored content. **The file is not copied** - all operations access the storage backend directly. From 4f15c90573fc3d65816969dc3294f0f4ad6be4fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 21:57:50 +0000 Subject: [PATCH 052/219] Add legacy type deprecation notice - attach@store and filepath@store maintained for backward compatibility - Will be deprecated with migration warnings in future releases - Eventually removed after transition period - New pipelines should use file type exclusively --- docs/src/design/tables/file-type-spec.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index c14deb2e0..46df66a02 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -612,6 +612,15 @@ azure = ["adlfs"] | Metadata | External table | External table | Inline JSON | | Deduplication | By content | By path | None | +### Legacy Type Deprecation + +The existing `attach@store` and `filepath@store` types will be: +- **Maintained** for backward compatibility with existing pipelines +- **Deprecated** in future releases with migration warnings +- **Eventually removed** after a transition period + +New pipelines should use the `file` type exclusively. + ## Delete Behavior When a record with a `file` attribute is deleted: From af6cef2eb2c210390df370b4553bc5d16ffae013 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 22:26:56 +0000 Subject: [PATCH 053/219] Add store metadata and client verification mechanism Store metadata (dj-store-meta.json): - Located at store root with project_name, created, format_version - Lists schemas using the store - Created on first file operation Client verification: - project_name required in client settings - Must match store metadata on connect - Raises DataJointError on mismatch - Ensures all clients use same configuration Also renamed hash_length to token_length throughout spec. --- docs/src/design/tables/file-type-spec.md | 104 +++++++++++++++++++++-- 1 file changed, 96 insertions(+), 8 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 46df66a02..087e31789 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -69,11 +69,12 @@ Object storage is configured in `datajoint.json` using the existing settings sys "database.host": "localhost", "database.user": "datajoint", + "object_storage.project_name": "my_project", "object_storage.protocol": "s3", "object_storage.endpoint": "s3.amazonaws.com", "object_storage.bucket": "my-bucket", "object_storage.location": "my_project", - "object_storage.partition_pattern": "subject{subject_id}/session{session_id}" + "object_storage.partition_pattern": "{subject_id}/{session_id}" } ``` @@ -81,9 +82,10 @@ For local filesystem storage: ```json { + "object_storage.project_name": "my_project", "object_storage.protocol": "file", "object_storage.location": "/data/my_project", - "object_storage.partition_pattern": "subject{subject_id}/session{session_id}" + "object_storage.partition_pattern": "{subject_id}/{session_id}" } ``` @@ -91,12 +93,13 @@ For local filesystem storage: | Setting | Type | Required | Description | |---------|------|----------|-------------| +| `object_storage.project_name` | string | Yes | Unique project identifier (must match store metadata) | | `object_storage.protocol` | string | Yes | Storage backend: `file`, `s3`, `gcs`, `azure` | | `object_storage.location` | string | Yes | Base path or bucket prefix | | `object_storage.bucket` | string | For cloud | Bucket name (S3, GCS, Azure) | | `object_storage.endpoint` | string | For S3 | S3 endpoint URL | | `object_storage.partition_pattern` | string | No | Path pattern with `{attribute}` placeholders | -| `object_storage.hash_length` | int | No | Random suffix length for filenames (default: 8, range: 4-16) | +| `object_storage.token_length` | int | No | Random suffix length for filenames (default: 8, range: 4-16) | | `object_storage.access_key` | string | For cloud | Access key (can use secrets file) | | `object_storage.secret_key` | string | For cloud | Secret key (can use secrets file) | @@ -139,6 +142,90 @@ s3://my-bucket/my_project/subject123/session45/schema_name/objects/Recording-raw If no partition pattern is specified, files are organized directly under `{location}/{schema}/objects/`. +## Store Metadata (`dj-store-meta.json`) + +Each object store contains a metadata file at its root that identifies the store and enables verification by DataJoint clients. + +### Location + +``` +{location}/dj-store-meta.json +``` + +For cloud storage: +``` +s3://bucket/my_project/dj-store-meta.json +``` + +### Content + +```json +{ + "project_name": "my_project", + "created": "2025-01-15T10:30:00Z", + "format_version": "1.0", + "datajoint_version": "0.15.0", + "schemas": ["schema1", "schema2"] +} +``` + +### Schema + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `project_name` | string | Yes | Unique project identifier | +| `created` | string | Yes | ISO 8601 timestamp of store creation | +| `format_version` | string | Yes | Store format version for compatibility | +| `datajoint_version` | string | Yes | DataJoint version that created the store | +| `schemas` | array | No | List of schemas using this store (updated on schema creation) | + +### Store Initialization + +The store metadata file is created when the first `file` attribute is used: + +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Client attempts first file operation │ +├─────────────────────────────────────────────────────────┤ +│ 2. Check if dj-store-meta.json exists │ +│ ├─ If exists: verify project_name matches │ +│ └─ If not: create with current project_name │ +├─────────────────────────────────────────────────────────┤ +│ 3. On mismatch: raise DataJointError │ +└─────────────────────────────────────────────────────────┘ +``` + +### Client Verification + +All DataJoint clients must use **identical `project_name`** settings to ensure store-database cohesion: + +1. **On connect**: Client reads `dj-store-meta.json` from store +2. **Verify**: `project_name` in client settings matches store metadata +3. **On mismatch**: Raise `DataJointError` with descriptive message + +```python +# Example error +DataJointError: Object store project name mismatch. + Client configured: "project_a" + Store metadata: "project_b" + Ensure all clients use the same object_storage.project_name setting. +``` + +### Schema Registration + +When a schema first uses the `file` type, it is added to the `schemas` list in the metadata: + +```python +# After creating Recording table with file attribute in my_schema +# dj-store-meta.json is updated: +{ + "project_name": "my_project", + "schemas": ["my_schema"] # my_schema added +} +``` + +This provides a record of which schemas have data in the store. + ## Syntax ```python @@ -211,7 +298,7 @@ Storage paths are **deterministically constructed** from record metadata, enabli 5. **Table name** - the table class name 6. **Primary key encoding** - remaining PK attributes and values 7. **Field name** - the attribute name -8. **Suffixed filename** - original name with random hash suffix +8. **Suffixed filename** - original name with random token suffix ### Path Template @@ -310,7 +397,7 @@ description=a1b2c3d4_abc123 # long string truncated + hash ### Filename Collision Avoidance -To prevent filename collisions, each stored file receives a **random hash suffix** appended to its basename: +To prevent filename collisions, each stored file receives a **random token suffix** appended to its basename: ``` original: recording.dat @@ -320,10 +407,10 @@ original: image.analysis.tiff stored: image.analysis_pL9nR4wE.tiff ``` -#### Hash Suffix Specification +#### Token Suffix Specification - **Alphabet**: URL-safe and filename-safe Base64 characters: `A-Z`, `a-z`, `0-9`, `-`, `_` -- **Length**: Configurable via `object_storage.hash_length` (default: 8, range: 4-16) +- **Length**: Configurable via `object_storage.token_length` (default: 8, range: 4-16) - **Generation**: Cryptographically random using `secrets.token_urlsafe()` At 8 characters with 64 possible values per character: 64^8 = 281 trillion combinations. @@ -511,12 +598,13 @@ class ObjectStorageSettings(BaseSettings): validate_assignment=True, ) + project_name: str | None = None # Must match store metadata protocol: Literal["file", "s3", "gcs", "azure"] | None = None location: str | None = None bucket: str | None = None endpoint: str | None = None partition_pattern: str | None = None - hash_length: int = Field(default=8, ge=4, le=16) + token_length: int = Field(default=8, ge=4, le=16) access_key: str | None = None secret_key: SecretStr | None = None ``` From ec2e73754f4a717a940bd369af0b338535a4785d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 22:34:37 +0000 Subject: [PATCH 054/219] Simplify store metadata - remove schema tracking - Removed schemas array from dj-store-meta.json - 1:1 correspondence between database+project_name and store assumed - DataJoint performs basic project_name verification on connect - Enforcement is administrative responsibility, not DataJoint's --- docs/src/design/tables/file-type-spec.md | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 087e31789..eff586cea 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -164,8 +164,7 @@ s3://bucket/my_project/dj-store-meta.json "project_name": "my_project", "created": "2025-01-15T10:30:00Z", "format_version": "1.0", - "datajoint_version": "0.15.0", - "schemas": ["schema1", "schema2"] + "datajoint_version": "0.15.0" } ``` @@ -177,7 +176,6 @@ s3://bucket/my_project/dj-store-meta.json | `created` | string | Yes | ISO 8601 timestamp of store creation | | `format_version` | string | Yes | Store format version for compatibility | | `datajoint_version` | string | Yes | DataJoint version that created the store | -| `schemas` | array | No | List of schemas using this store (updated on schema creation) | ### Store Initialization @@ -197,7 +195,7 @@ The store metadata file is created when the first `file` attribute is used: ### Client Verification -All DataJoint clients must use **identical `project_name`** settings to ensure store-database cohesion: +DataJoint performs a basic verification on connect to ensure store-database cohesion: 1. **On connect**: Client reads `dj-store-meta.json` from store 2. **Verify**: `project_name` in client settings matches store metadata @@ -211,20 +209,13 @@ DataJointError: Object store project name mismatch. Ensure all clients use the same object_storage.project_name setting. ``` -### Schema Registration +### Administrative Responsibility -When a schema first uses the `file` type, it is added to the `schemas` list in the metadata: +A 1:1 correspondence is assumed between: +- Database location + `project_name` in client settings +- Object store + `project_name` in store metadata -```python -# After creating Recording table with file attribute in my_schema -# dj-store-meta.json is updated: -{ - "project_name": "my_project", - "schemas": ["my_schema"] # my_schema added -} -``` - -This provides a record of which schemas have data in the store. +DataJoint performs basic verification but does **not** enforce this mapping. Administrators are responsible for ensuring correct configuration across all clients. ## Syntax From b32ef8dd153c33a8468ca511647dced5e3adc810 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 22:44:17 +0000 Subject: [PATCH 055/219] Rename type from 'file' to 'object' - Type syntax: `object` instead of `file` - Class: ObjectRef instead of FileRef - Module: objectref.py instead of fileref.py - Pattern: OBJECT matching `object$` - JSON fields: is_dir, item_count (renamed from is_folder, file_count) - Consistent with object_storage.* settings namespace - Aligns with objects/ directory in path structure --- docs/src/design/tables/file-type-spec.md | 60 ++++++++++++------------ 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index eff586cea..14be74d68 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -1,14 +1,14 @@ -# File Column Type Specification +# Object Column Type Specification ## Overview -The `file` type introduces a new paradigm for managed file storage in DataJoint. Unlike existing `attach@store` and `filepath@store` types that reference named stores, the `file` type uses a **unified storage backend** that is tightly coupled with the schema and configured at the pipeline level. +The `object` type introduces a new paradigm for managed file storage in DataJoint. Unlike existing `attach@store` and `filepath@store` types that reference named stores, the `object` type uses a **unified storage backend** that is tightly coupled with the schema and configured at the pipeline level. -The `file` type supports both **files and folders**. Content is copied to storage at insert time, referenced via handle on fetch, and deleted when the record is deleted. +The `object` type supports both **files and folders**. Content is copied to storage at insert time, referenced via handle on fetch, and deleted when the record is deleted. ### Immutability Contract -Files stored via the `file` type are **immutable**. Users agree to: +Files stored via the `object` type are **immutable**. Users agree to: - **Insert**: Copy content to storage (only way to create) - **Fetch**: Read content via handle (no modification) - **Delete**: Remove content when record is deleted (only way to remove) @@ -179,7 +179,7 @@ s3://bucket/my_project/dj-store-meta.json ### Store Initialization -The store metadata file is created when the first `file` attribute is used: +The store metadata file is created when the first `object` attribute is used: ``` ┌─────────────────────────────────────────────────────────┐ @@ -226,8 +226,8 @@ class Recording(dj.Manual): subject_id : int session_id : int --- - raw_data : file # managed file storage - processed : file # another file attribute + raw_data : object # managed file storage + processed : object # another object attribute """ ``` @@ -235,7 +235,7 @@ Note: No `@store` suffix needed - storage is determined by pipeline configuratio ## Database Storage -The `file` type is stored as a `JSON` column in MySQL containing: +The `object` type is stored as a `JSON` column in MySQL containing: **File example:** ```json @@ -244,7 +244,7 @@ The `file` type is stored as a `JSON` column in MySQL containing: "size": 12345, "hash": "sha256:abcdef1234...", "original_name": "recording.dat", - "is_folder": false, + "is_dir": false, "timestamp": "2025-01-15T10:30:00Z", "mime_type": "application/octet-stream" } @@ -257,9 +257,9 @@ The `file` type is stored as a `JSON` column in MySQL containing: "size": 567890, "hash": "sha256:fedcba9876...", "original_name": "data_folder", - "is_folder": true, + "is_dir": true, "timestamp": "2025-01-15T10:30:00Z", - "file_count": 42 + "item_count": 42 } ``` @@ -271,10 +271,10 @@ The `file` type is stored as a `JSON` column in MySQL containing: | `size` | integer | Yes | Total size in bytes (sum for folders) | | `hash` | string | Yes | Content hash with algorithm prefix | | `original_name` | string | Yes | Original file/folder name at insert time | -| `is_folder` | boolean | Yes | True if stored content is a directory | +| `is_dir` | boolean | Yes | True if stored content is a directory | | `timestamp` | string | Yes | ISO 8601 upload timestamp | | `mime_type` | string | No | MIME type (files only, auto-detected or provided) | -| `file_count` | integer | No | Number of files (folders only) | +| `item_count` | integer | No | Number of files (folders only) | ## Path Generation @@ -329,7 +329,7 @@ class Recording(dj.Manual): subject_id : int session_id : int --- - raw_data : file + raw_data : object """ ``` @@ -422,7 +422,7 @@ Each insert stores a separate copy of the file, even if identical content was pr ## Insert Behavior -At insert time, the `file` attribute accepts: +At insert time, the `object` attribute accepts: 1. **File path** (string or `Path`): Path to an existing file 2. **Folder path** (string or `Path`): Path to an existing directory @@ -531,7 +531,7 @@ schema.file_storage.cleanup_orphaned() # Delete orphaned files ## Fetch Behavior -On fetch, the `file` type returns a **handle** (`FileRef` object) to the stored content. **The file is not copied** - all operations access the storage backend directly. +On fetch, the `object` type returns a **handle** (`ObjectRef` object) to the stored content. **The file is not copied** - all operations access the storage backend directly. ```python record = Recording.fetch1() @@ -542,7 +542,7 @@ print(file_ref.path) # Full storage path print(file_ref.size) # File size in bytes print(file_ref.hash) # Content hash print(file_ref.original_name) # Original filename -print(file_ref.is_folder) # True if stored content is a folder +print(file_ref.is_dir) # True if stored content is a folder # Read content directly from storage backend content = file_ref.read() # Returns bytes (files only) @@ -561,7 +561,7 @@ with file_ref.open("subdir/file.dat") as f: ### No Automatic Download -Unlike `attach@store`, the `file` type does **not** automatically download content to a local path. Users access content directly through the `FileRef` handle, which streams from the storage backend. +Unlike `attach@store`, the `object` type does **not** automatically download content to a local path. Users access content directly through the `ObjectRef` handle, which streams from the storage backend. For local copies, users explicitly download: @@ -581,7 +581,7 @@ New `ObjectStorageSettings` class: ```python class ObjectStorageSettings(BaseSettings): - """Object storage configuration for file columns.""" + """Object storage configuration for object columns.""" model_config = SettingsConfigDict( env_prefix="DJ_OBJECT_STORAGE_", @@ -590,7 +590,7 @@ class ObjectStorageSettings(BaseSettings): ) project_name: str | None = None # Must match store metadata - protocol: Literal["file", "s3", "gcs", "azure"] | None = None + protocol: Literal["object", "s3", "gcs", "azure"] | None = None location: str | None = None bucket: str | None = None endpoint: str | None = None @@ -614,7 +614,7 @@ object_storage: ObjectStorageSettings = Field(default_factory=ObjectStorageSetti ### 3. Type Declaration (`declare.py`) -- Add `FILE` pattern: `file$` +- Add `OBJECT` pattern: `object$` - Add to `SPECIAL_TYPES` - Substitute to `JSON` type in database @@ -631,25 +631,25 @@ object_storage: ObjectStorageSettings = Field(default_factory=ObjectStorageSetti ### 6. Fetch Processing (`fetch.py`) -- New `FileRef` class +- New `ObjectRef` class - Lazy loading from storage backend - Metadata access interface -### 7. FileRef Class (`fileref.py` - new module) +### 7. ObjectRef Class (`objectref.py` - new module) ```python @dataclass -class FileRef: +class ObjectRef: """Handle to a file or folder stored in the pipeline's storage backend.""" path: str size: int hash: str original_name: str - is_folder: bool + is_dir: bool timestamp: datetime mime_type: str | None # files only - file_count: int | None # folders only + item_count: int | None # folders only _backend: StorageBackend # internal reference # File operations @@ -681,7 +681,7 @@ azure = ["adlfs"] ## Comparison with Existing Types -| Feature | `attach@store` | `filepath@store` | `file` | +| Feature | `attach@store` | `filepath@store` | `object` | |---------|----------------|------------------|--------| | Store config | Per-attribute | Per-attribute | Per-pipeline | | Path control | DataJoint | User-managed | DataJoint | @@ -698,11 +698,11 @@ The existing `attach@store` and `filepath@store` types will be: - **Deprecated** in future releases with migration warnings - **Eventually removed** after a transition period -New pipelines should use the `file` type exclusively. +New pipelines should use the `object` type exclusively. ## Delete Behavior -When a record with a `file` attribute is deleted: +When a record with a `object` attribute is deleted: 1. **Database delete executes first** (within transaction) 2. **File delete is attempted** after successful DB commit @@ -736,7 +736,7 @@ Each record owns its file exclusively. There is no deduplication or reference co ## Migration Path - Existing `attach@store` and `filepath@store` remain unchanged -- `file` type is additive - new tables only +- `object` type is additive - new tables only - Future: Migration utilities to convert existing external storage ## Future Extensions From 93ce01e2773e6ad3ccdc628173eb39d9805cd128 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 22:53:38 +0000 Subject: [PATCH 056/219] Add Zarr compatibility: staged insert and fsspec access Staged Insert (direct write mode): - stage_object() context manager for writing directly to storage - StagedObject provides fs, store, full_path for Zarr/xarray - Cleanup on failure, metadata computed on success - Avoids copy overhead for large arrays ObjectRef fsspec accessors: - fs property: returns fsspec filesystem - store property: returns FSMap for Zarr/xarray - full_path property: returns full URI Updated immutability contract: - Objects immutable "after finalization" - Two insert modes: copy (existing data) and staged (direct write) --- docs/src/design/tables/file-type-spec.md | 144 ++++++++++++++++++++++- 1 file changed, 141 insertions(+), 3 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 14be74d68..87b206ab5 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -8,12 +8,20 @@ The `object` type supports both **files and folders**. Content is copied to stor ### Immutability Contract -Files stored via the `object` type are **immutable**. Users agree to: -- **Insert**: Copy content to storage (only way to create) +Objects stored via the `object` type are **immutable after finalization**. Users agree to: +- **Insert (copy)**: Copy existing content to storage +- **Insert (staged)**: Reserve path, write directly, then finalize - **Fetch**: Read content via handle (no modification) - **Delete**: Remove content when record is deleted (only way to remove) -Users must not directly modify files in the object store. +Once an object is **finalized** (either via copy-insert or staged-insert completion), users must not directly modify it in the object store. + +#### Two Insert Modes + +| Mode | Use Case | Workflow | +|------|----------|----------| +| **Copy** | Small files, existing data | Local file → copy to storage → insert record | +| **Staged** | Large objects, Zarr/HDF5 | Reserve path → write directly to storage → finalize record | ## Storage Architecture @@ -470,6 +478,97 @@ The file/folder is copied to storage **before** the database insert is attempted - If the copy succeeds but the database insert fails, an orphaned file may remain - Orphaned files are acceptable due to the random token (no collision with future inserts) +### Staged Insert (Direct Write Mode) + +For large objects like Zarr arrays, copying from local storage is inefficient. **Staged insert** allows writing directly to the destination: + +```python +# Stage an object for direct writing +with Recording.stage_object( + {"subject_id": 123, "session_id": 45}, + "raw_data", + "my_array.zarr" +) as staged: + # Write directly to object storage (no local copy) + import zarr + z = zarr.open(staged.store, mode='w', shape=(10000, 10000), dtype='f4') + z[:] = compute_large_array() + +# On successful exit: metadata computed, record inserted +# On exception: storage cleaned up, no record inserted +``` + +#### StagedObject Interface + +```python +@dataclass +class StagedObject: + """Handle for staged write operations.""" + + path: str # Reserved storage path + full_path: str # Full URI (e.g., 's3://bucket/path') + fs: fsspec.AbstractFileSystem # fsspec filesystem + store: fsspec.FSMap # FSMap for Zarr/xarray + + def open(self, subpath: str = "", mode: str = "wb") -> IO: + """Open a file within the staged object for writing.""" + ... +``` + +#### Staged Insert Flow + +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Reserve storage path with random token │ +├─────────────────────────────────────────────────────────┤ +│ 2. Return StagedObject handle to user │ +├─────────────────────────────────────────────────────────┤ +│ 3. User writes data directly via fs/store │ +├─────────────────────────────────────────────────────────┤ +│ 4. On context exit (success): │ +│ - Compute metadata (size, hash, item_count) │ +│ - Execute database INSERT │ +├─────────────────────────────────────────────────────────┤ +│ 5. On context exit (exception): │ +│ - Delete any written data │ +│ - Re-raise exception │ +└─────────────────────────────────────────────────────────┘ +``` + +#### Zarr Example + +```python +import zarr +import numpy as np + +# Create a large Zarr array directly in object storage +with Recording.stage_object( + {"subject_id": 123, "session_id": 45}, + "neural_data", + "spikes.zarr" +) as staged: + # Create Zarr hierarchy + root = zarr.open(staged.store, mode='w') + root.create_dataset('timestamps', data=np.arange(1000000)) + root.create_dataset('waveforms', shape=(1000000, 82), chunks=(10000, 82)) + + # Write in chunks (streaming from acquisition) + for i, chunk in enumerate(data_stream): + root['waveforms'][i*10000:(i+1)*10000] = chunk + +# Record automatically inserted with computed metadata +``` + +#### Comparison: Copy vs Staged Insert + +| Aspect | Copy Insert | Staged Insert | +|--------|-------------|---------------| +| Data location | Must exist locally first | Written directly to storage | +| Efficiency | Copy overhead | No copy needed | +| Use case | Small files, existing data | Large arrays, streaming data | +| Cleanup on failure | Orphan possible | Cleaned up | +| API | `insert1({..., "field": path})` | `stage_object()` context manager | + ## Transaction Handling Since storage backends don't support distributed transactions with MySQL, DataJoint uses a **copy-first** strategy. @@ -652,6 +751,22 @@ class ObjectRef: item_count: int | None # folders only _backend: StorageBackend # internal reference + # fsspec access (for Zarr, xarray, etc.) + @property + def fs(self) -> fsspec.AbstractFileSystem: + """Return fsspec filesystem for direct access.""" + ... + + @property + def store(self) -> fsspec.FSMap: + """Return FSMap suitable for Zarr/xarray.""" + ... + + @property + def full_path(self) -> str: + """Return full URI (e.g., 's3://bucket/path').""" + ... + # File operations def read(self) -> bytes: ... def open(self, subpath: str | None = None, mode: str = "rb") -> IO: ... @@ -665,6 +780,29 @@ class ObjectRef: def exists(self, subpath: str | None = None) -> bool: ... ``` +#### fsspec Integration + +The `ObjectRef` provides direct fsspec access for integration with array libraries: + +```python +import zarr +import xarray as xr + +record = Recording.fetch1() +obj_ref = record["raw_data"] + +# Direct Zarr access +z = zarr.open(obj_ref.store, mode='r') +print(z.shape) + +# Direct xarray access +ds = xr.open_zarr(obj_ref.store) + +# Use fsspec filesystem directly +fs = obj_ref.fs +files = fs.ls(obj_ref.full_path) +``` + ## Dependencies New dependency: `fsspec` with optional backend-specific packages: From 997d992e38eaa6486d2184a43a54a5150e352f69 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 23:16:22 +0000 Subject: [PATCH 057/219] Finalize staged_insert1 API for direct object storage writes - Use dedicated staged_insert1 method instead of co-opting insert1 - Add StagedInsert class with rec dict, store(), and open() methods - Document rationale for separate method (explicit, backward compatible, type safe) - Add examples for Zarr and multiple object fields - Note that staged inserts are limited to insert1 (no multi-row) --- docs/src/design/tables/file-type-spec.md | 130 +++++++++++++++++------ 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 87b206ab5..ca0e2f475 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -480,38 +480,77 @@ The file/folder is copied to storage **before** the database insert is attempted ### Staged Insert (Direct Write Mode) -For large objects like Zarr arrays, copying from local storage is inefficient. **Staged insert** allows writing directly to the destination: +For large objects like Zarr arrays, copying from local storage is inefficient. **Staged insert** allows writing directly to the destination. + +#### Why a Separate Method? + +Staged insert uses a dedicated `staged_insert1` method rather than co-opting `insert1` because: + +1. **Explicit over implicit** - Staged inserts have fundamentally different semantics (file creation happens during context, commit on exit). A separate method makes this explicit. +2. **Backward compatibility** - `insert1` returns `None` and doesn't support context manager protocol. Changing this could break existing code. +3. **Clear error handling** - The context manager semantics (success = commit, exception = rollback) are obvious with `staged_insert1`. +4. **Type safety** - The staged context exposes `.store()` for object fields. A dedicated method can return a properly-typed `StagedInsert` object. + +**Staged inserts are limited to `insert1`** (one row at a time). Multi-row inserts are not supported for staged operations. + +#### Basic Usage ```python -# Stage an object for direct writing -with Recording.stage_object( - {"subject_id": 123, "session_id": 45}, - "raw_data", - "my_array.zarr" -) as staged: - # Write directly to object storage (no local copy) - import zarr - z = zarr.open(staged.store, mode='w', shape=(10000, 10000), dtype='f4') +# Stage an insert with direct object storage writes +with Recording.staged_insert1 as staged: + # Set primary key values + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Create object storage directly using store() + z = zarr.open(staged.store('raw_data', 'my_array.zarr'), mode='w', shape=(10000, 10000), dtype='f4') z[:] = compute_large_array() + # Assign the created object to the record + staged.rec['raw_data'] = z + # On successful exit: metadata computed, record inserted # On exception: storage cleaned up, no record inserted ``` -#### StagedObject Interface +#### StagedInsert Interface ```python -@dataclass -class StagedObject: - """Handle for staged write operations.""" +class StagedInsert: + """Context manager for staged insert operations.""" - path: str # Reserved storage path - full_path: str # Full URI (e.g., 's3://bucket/path') - fs: fsspec.AbstractFileSystem # fsspec filesystem - store: fsspec.FSMap # FSMap for Zarr/xarray + rec: dict[str, Any] # Record dict for setting attribute values - def open(self, subpath: str = "", mode: str = "wb") -> IO: - """Open a file within the staged object for writing.""" + def store(self, field: str, name: str) -> fsspec.FSMap: + """ + Get an FSMap store for direct writes to an object field. + + Args: + field: Name of the object attribute + name: Filename/dirname for the stored object + + Returns: + fsspec.FSMap suitable for Zarr/xarray + """ + ... + + def open(self, field: str, name: str, mode: str = "wb") -> IO: + """ + Open a file for direct writes to an object field. + + Args: + field: Name of the object attribute + name: Filename for the stored object + mode: File mode (default: "wb") + + Returns: + File-like object for writing + """ + ... + + @property + def fs(self) -> fsspec.AbstractFileSystem: + """Return fsspec filesystem for advanced operations.""" ... ``` @@ -519,17 +558,21 @@ class StagedObject: ``` ┌─────────────────────────────────────────────────────────┐ -│ 1. Reserve storage path with random token │ +│ 1. Enter context: create StagedInsert with empty rec │ +├─────────────────────────────────────────────────────────┤ +│ 2. User sets primary key values in staged.rec │ ├─────────────────────────────────────────────────────────┤ -│ 2. Return StagedObject handle to user │ +│ 3. User calls store()/open() to get storage handles │ +│ - Path reserved with random token on first call │ +│ - User writes data directly via fsspec │ ├─────────────────────────────────────────────────────────┤ -│ 3. User writes data directly via fs/store │ +│ 4. User assigns object references to staged.rec │ ├─────────────────────────────────────────────────────────┤ -│ 4. On context exit (success): │ +│ 5. On context exit (success): │ │ - Compute metadata (size, hash, item_count) │ │ - Execute database INSERT │ ├─────────────────────────────────────────────────────────┤ -│ 5. On context exit (exception): │ +│ 6. On context exit (exception): │ │ - Delete any written data │ │ - Re-raise exception │ └─────────────────────────────────────────────────────────┘ @@ -542,13 +585,12 @@ import zarr import numpy as np # Create a large Zarr array directly in object storage -with Recording.stage_object( - {"subject_id": 123, "session_id": 45}, - "neural_data", - "spikes.zarr" -) as staged: - # Create Zarr hierarchy - root = zarr.open(staged.store, mode='w') +with Recording.staged_insert1 as staged: + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Create Zarr hierarchy directly in object storage + root = zarr.open(staged.store('neural_data', 'spikes.zarr'), mode='w') root.create_dataset('timestamps', data=np.arange(1000000)) root.create_dataset('waveforms', shape=(1000000, 82), chunks=(10000, 82)) @@ -556,9 +598,30 @@ with Recording.stage_object( for i, chunk in enumerate(data_stream): root['waveforms'][i*10000:(i+1)*10000] = chunk + # Assign to record + staged.rec['neural_data'] = root + # Record automatically inserted with computed metadata ``` +#### Multiple Object Fields + +```python +with Recording.staged_insert1 as staged: + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Write multiple object fields + raw = zarr.open(staged.store('raw_data', 'raw.zarr'), mode='w', shape=(1000, 1000)) + raw[:] = raw_array + + processed = zarr.open(staged.store('processed', 'processed.zarr'), mode='w', shape=(100, 100)) + processed[:] = processed_array + + staged.rec['raw_data'] = raw + staged.rec['processed'] = processed +``` + #### Comparison: Copy vs Staged Insert | Aspect | Copy Insert | Staged Insert | @@ -567,7 +630,8 @@ with Recording.stage_object( | Efficiency | Copy overhead | No copy needed | | Use case | Small files, existing data | Large arrays, streaming data | | Cleanup on failure | Orphan possible | Cleaned up | -| API | `insert1({..., "field": path})` | `stage_object()` context manager | +| API | `insert1({..., "field": path})` | `staged_insert1` context manager | +| Multi-row | Supported | Not supported (insert1 only) | ## Transaction Handling From 36806cccdca2616a6f2af247963c7e2bda2d090a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 23:30:15 +0000 Subject: [PATCH 058/219] Simplify object naming: field name as base, extension from source - Filename is always {field}_{token}{ext}, no user control over base name - Extension extracted from source file (copy) or optionally provided (staged) - Replace `original_name` with `ext` in JSON schema and ObjectRef - Update path templates, examples, and StagedInsert interface - Add "Filename Convention" section explaining the design --- docs/src/design/tables/file-type-spec.md | 109 +++++++++++++++-------- 1 file changed, 71 insertions(+), 38 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index ca0e2f475..4962417d4 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -248,10 +248,10 @@ The `object` type is stored as a `JSON` column in MySQL containing: **File example:** ```json { - "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data/recording_Ax7bQ2kM.dat", + "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat", "size": 12345, "hash": "sha256:abcdef1234...", - "original_name": "recording.dat", + "ext": ".dat", "is_dir": false, "timestamp": "2025-01-15T10:30:00Z", "mime_type": "application/octet-stream" @@ -261,10 +261,10 @@ The `object` type is stored as a `JSON` column in MySQL containing: **Folder example:** ```json { - "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data/data_folder_pL9nR4wE", + "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data_pL9nR4wE", "size": 567890, "hash": "sha256:fedcba9876...", - "original_name": "data_folder", + "ext": null, "is_dir": true, "timestamp": "2025-01-15T10:30:00Z", "item_count": 42 @@ -278,12 +278,33 @@ The `object` type is stored as a `JSON` column in MySQL containing: | `path` | string | Yes | Full path/key within storage backend (includes token) | | `size` | integer | Yes | Total size in bytes (sum for folders) | | `hash` | string | Yes | Content hash with algorithm prefix | -| `original_name` | string | Yes | Original file/folder name at insert time | +| `ext` | string/null | Yes | File extension (e.g., `.dat`, `.zarr`) or null | | `is_dir` | boolean | Yes | True if stored content is a directory | | `timestamp` | string | Yes | ISO 8601 upload timestamp | -| `mime_type` | string | No | MIME type (files only, auto-detected or provided) | +| `mime_type` | string | No | MIME type (files only, auto-detected from extension) | | `item_count` | integer | No | Number of files (folders only) | +### Filename Convention + +The stored filename is **always derived from the field name**: +- **Base name**: The attribute/field name (e.g., `raw_data`) +- **Extension**: Adopted from source file (copy insert) or optionally provided (staged insert) +- **Token**: Random suffix for collision avoidance + +``` +Stored filename = {field}_{token}{ext} + +Examples: + raw_data_Ax7bQ2kM.dat # file with .dat extension + raw_data_pL9nR4wE.zarr # Zarr directory with .zarr extension + raw_data_kM3nP2qR # directory without extension +``` + +This convention ensures: +- Consistent, predictable naming across all objects +- Field name visible in storage for easier debugging +- Extension preserved for MIME type detection and tooling compatibility + ## Path Generation Storage paths are **deterministically constructed** from record metadata, enabling bidirectional lookup between database records and stored files. @@ -296,19 +317,18 @@ Storage paths are **deterministically constructed** from record metadata, enabli 4. **Object directory** - `objects/` 5. **Table name** - the table class name 6. **Primary key encoding** - remaining PK attributes and values -7. **Field name** - the attribute name -8. **Suffixed filename** - original name with random token suffix +7. **Suffixed filename** - `{field}_{token}{ext}` ### Path Template **Without partitioning:** ``` -{location}/{schema}/objects/{Table}/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../field/{basename}_{token}.{ext} +{location}/{schema}/objects/{Table}/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../{field}_{token}{ext} ``` **With partitioning:** ``` -{location}/{partition_attr}={val}/.../schema/objects/{Table}/{remaining_pk_attrs}/.../field/{basename}_{token}.{ext} +{location}/{partition_attr}={val}/.../schema/objects/{Table}/{remaining_pk_attrs}/.../{field}_{token}{ext} ``` ### Partitioning @@ -344,15 +364,17 @@ class Recording(dj.Manual): Inserting `{"subject_id": 123, "session_id": 45, "raw_data": "/path/to/recording.dat"}` produces: ``` -my_project/my_schema/objects/Recording/subject_id=123/session_id=45/raw_data/recording_Ax7bQ2kM.dat +my_project/my_schema/objects/Recording/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat ``` +Note: The filename is `raw_data` (field name) with `.dat` extension (from source file). + ### Example With Partitioning With `partition_pattern = "{subject_id}"`: ``` -my_project/subject_id=123/my_schema/objects/Recording/session_id=45/raw_data/recording_Ax7bQ2kM.dat +my_project/subject_id=123/my_schema/objects/Recording/session_id=45/raw_data_Ax7bQ2kM.dat ``` The `subject_id` is promoted to the path root, grouping all files for subject 123 together regardless of schema or table. @@ -396,14 +418,17 @@ description=a1b2c3d4_abc123 # long string truncated + hash ### Filename Collision Avoidance -To prevent filename collisions, each stored file receives a **random token suffix** appended to its basename: +To prevent filename collisions, each stored object receives a **random token suffix** appended to the field name: ``` -original: recording.dat -stored: recording_Ax7bQ2kM.dat +field: raw_data, source: recording.dat +stored: raw_data_Ax7bQ2kM.dat -original: image.analysis.tiff -stored: image.analysis_pL9nR4wE.tiff +field: image, source: scan.tiff +stored: image_pL9nR4wE.tiff + +field: neural_data (staged with .zarr) +stored: neural_data_kM3nP2qR.zarr ``` #### Token Suffix Specification @@ -417,7 +442,7 @@ At 8 characters with 64 possible values per character: 64^8 = 281 trillion combi #### Rationale - Avoids collisions without requiring existence checks -- Preserves original filename for human readability +- Field name visible in storage for easier debugging/auditing - URL-safe for web-based access to cloud storage - Filesystem-safe across all supported platforms @@ -432,33 +457,35 @@ Each insert stores a separate copy of the file, even if identical content was pr At insert time, the `object` attribute accepts: -1. **File path** (string or `Path`): Path to an existing file +1. **File path** (string or `Path`): Path to an existing file (extension extracted) 2. **Folder path** (string or `Path`): Path to an existing directory -3. **Stream object**: File-like object with `read()` method -4. **Tuple of (name, stream)**: Stream with explicit filename +3. **Tuple of (ext, stream)**: File-like object with explicit extension ```python -# From file path +# From file path - extension (.dat) extracted from source Recording.insert1({ "subject_id": 123, "session_id": 45, "raw_data": "/local/path/to/recording.dat" }) +# Stored as: raw_data_Ax7bQ2kM.dat -# From folder path +# From folder path - no extension Recording.insert1({ "subject_id": 123, "session_id": 45, "raw_data": "/local/path/to/data_folder/" }) +# Stored as: raw_data_pL9nR4wE/ -# From stream with explicit name +# From stream with explicit extension with open("/local/path/data.bin", "rb") as f: Recording.insert1({ "subject_id": 123, "session_id": 45, - "raw_data": ("custom_name.dat", f) + "raw_data": (".bin", f) }) +# Stored as: raw_data_kM3nP2qR.bin ``` ### Insert Processing Steps @@ -503,7 +530,8 @@ with Recording.staged_insert1 as staged: staged.rec['session_id'] = 45 # Create object storage directly using store() - z = zarr.open(staged.store('raw_data', 'my_array.zarr'), mode='w', shape=(10000, 10000), dtype='f4') + # Extension is optional - .zarr is conventional for Zarr arrays + z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(10000, 10000), dtype='f4') z[:] = compute_large_array() # Assign the created object to the record @@ -511,6 +539,7 @@ with Recording.staged_insert1 as staged: # On successful exit: metadata computed, record inserted # On exception: storage cleaned up, no record inserted +# Stored as: raw_data_Ax7bQ2kM.zarr ``` #### StagedInsert Interface @@ -521,26 +550,26 @@ class StagedInsert: rec: dict[str, Any] # Record dict for setting attribute values - def store(self, field: str, name: str) -> fsspec.FSMap: + def store(self, field: str, ext: str = "") -> fsspec.FSMap: """ Get an FSMap store for direct writes to an object field. Args: field: Name of the object attribute - name: Filename/dirname for the stored object + ext: Optional extension (e.g., ".zarr", ".hdf5") Returns: fsspec.FSMap suitable for Zarr/xarray """ ... - def open(self, field: str, name: str, mode: str = "wb") -> IO: + def open(self, field: str, ext: str = "", mode: str = "wb") -> IO: """ Open a file for direct writes to an object field. Args: field: Name of the object attribute - name: Filename for the stored object + ext: Optional extension (e.g., ".bin", ".dat") mode: File mode (default: "wb") Returns: @@ -590,7 +619,8 @@ with Recording.staged_insert1 as staged: staged.rec['session_id'] = 45 # Create Zarr hierarchy directly in object storage - root = zarr.open(staged.store('neural_data', 'spikes.zarr'), mode='w') + # .zarr extension is optional but conventional + root = zarr.open(staged.store('neural_data', '.zarr'), mode='w') root.create_dataset('timestamps', data=np.arange(1000000)) root.create_dataset('waveforms', shape=(1000000, 82), chunks=(10000, 82)) @@ -602,6 +632,7 @@ with Recording.staged_insert1 as staged: staged.rec['neural_data'] = root # Record automatically inserted with computed metadata +# Stored as: neural_data_kM3nP2qR.zarr ``` #### Multiple Object Fields @@ -611,15 +642,17 @@ with Recording.staged_insert1 as staged: staged.rec['subject_id'] = 123 staged.rec['session_id'] = 45 - # Write multiple object fields - raw = zarr.open(staged.store('raw_data', 'raw.zarr'), mode='w', shape=(1000, 1000)) + # Write multiple object fields - extension optional + raw = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(1000, 1000)) raw[:] = raw_array - processed = zarr.open(staged.store('processed', 'processed.zarr'), mode='w', shape=(100, 100)) + processed = zarr.open(staged.store('processed', '.zarr'), mode='w', shape=(100, 100)) processed[:] = processed_array staged.rec['raw_data'] = raw staged.rec['processed'] = processed + +# Stored as: raw_data_Ax7bQ2kM.zarr, processed_pL9nR4wE.zarr ``` #### Comparison: Copy vs Staged Insert @@ -704,8 +737,8 @@ file_ref = record["raw_data"] print(file_ref.path) # Full storage path print(file_ref.size) # File size in bytes print(file_ref.hash) # Content hash -print(file_ref.original_name) # Original filename -print(file_ref.is_dir) # True if stored content is a folder +print(file_ref.ext) # File extension (e.g., ".dat") or None +print(file_ref.is_dir) # True if stored content is a folder # Read content directly from storage backend content = file_ref.read() # Returns bytes (files only) @@ -808,10 +841,10 @@ class ObjectRef: path: str size: int hash: str - original_name: str + ext: str | None # file extension (e.g., ".dat") or None is_dir: bool timestamp: datetime - mime_type: str | None # files only + mime_type: str | None # files only, derived from ext item_count: int | None # folders only _backend: StorageBackend # internal reference From 6c6349b96e2093627e290324f8653b1729be525c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Dec 2025 23:50:06 +0000 Subject: [PATCH 059/219] Restructure store paths: objects/ after table, rename store config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename store metadata: dj-store-meta.json → datajoint_store.json - Move objects/ directory after table name in path hierarchy - Path is now: {schema}/{Table}/objects/{pk_attrs}/{field}_{token}{ext} - Allows table folders to contain both tabular data and objects - Update all path examples and JSON samples --- docs/src/design/tables/file-type-spec.md | 62 ++++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 4962417d4..941f0790f 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -42,19 +42,17 @@ A DataJoint project creates a structured hierarchical storage pattern: ``` 📁 project_name/ -├── datajoint.json -├── 📁 schema_name1/ -├── 📁 schema_name2/ -├── 📁 schema_name3/ -│ ├── schema.py -│ ├── 📁 tables/ -│ │ ├── table1/key1-value1.parquet -│ │ ├── table2/key2-value2.parquet -│ │ ... -│ ├── 📁 objects/ -│ │ ├── table1-field1/key3-value3.zarr -│ │ ├── table1-field2/key3-value3.gif -│ │ ... +├── datajoint_store.json # store metadata (not client config) +├── 📁 schema_name/ +│ ├── 📁 Table1/ +│ │ ├── data.parquet # tabular data export (future) +│ │ └── 📁 objects/ # object storage for this table +│ │ ├── pk1=val1/pk2=val2/field1_token.dat +│ │ └── pk1=val1/pk2=val2/field2_token.zarr +│ ├── 📁 Table2/ +│ │ ├── data.parquet +│ │ └── 📁 objects/ +│ │ └── ... ``` ### Object Storage Keys @@ -62,8 +60,8 @@ A DataJoint project creates a structured hierarchical storage pattern: When using cloud object storage: ``` -s3://bucket/project_name/schema_name3/objects/table1/key1-value1.parquet -s3://bucket/project_name/schema_name3/objects/table1-field1/key3-value3.zarr +s3://bucket/project_name/schema_name/Table1/objects/pk1=val1/field_token.dat +s3://bucket/project_name/schema_name/Table1/objects/pk1=val1/field_token.zarr ``` ## Configuration @@ -145,24 +143,24 @@ The partition pattern is configured **per pipeline** (one per settings file). Pl **Example with partitioning:** ``` -s3://my-bucket/my_project/subject123/session45/schema_name/objects/Recording-raw_data/recording.dat +s3://my-bucket/my_project/subject_id=123/session_id=45/schema_name/Recording/objects/raw_data_Ax7bQ2kM.dat ``` -If no partition pattern is specified, files are organized directly under `{location}/{schema}/objects/`. +If no partition pattern is specified, files are organized directly under `{location}/{schema}/{Table}/objects/`. -## Store Metadata (`dj-store-meta.json`) +## Store Metadata (`datajoint_store.json`) -Each object store contains a metadata file at its root that identifies the store and enables verification by DataJoint clients. +Each object store contains a metadata file at its root that identifies the store and enables verification by DataJoint clients. This file is named `datajoint_store.json` to distinguish it from client configuration files (`datajoint.json`). ### Location ``` -{location}/dj-store-meta.json +{location}/datajoint_store.json ``` For cloud storage: ``` -s3://bucket/my_project/dj-store-meta.json +s3://bucket/my_project/datajoint_store.json ``` ### Content @@ -193,7 +191,7 @@ The store metadata file is created when the first `object` attribute is used: ┌─────────────────────────────────────────────────────────┐ │ 1. Client attempts first file operation │ ├─────────────────────────────────────────────────────────┤ -│ 2. Check if dj-store-meta.json exists │ +│ 2. Check if datajoint_store.json exists │ │ ├─ If exists: verify project_name matches │ │ └─ If not: create with current project_name │ ├─────────────────────────────────────────────────────────┤ @@ -205,7 +203,7 @@ The store metadata file is created when the first `object` attribute is used: DataJoint performs a basic verification on connect to ensure store-database cohesion: -1. **On connect**: Client reads `dj-store-meta.json` from store +1. **On connect**: Client reads `datajoint_store.json` from store 2. **Verify**: `project_name` in client settings matches store metadata 3. **On mismatch**: Raise `DataJointError` with descriptive message @@ -248,7 +246,7 @@ The `object` type is stored as a `JSON` column in MySQL containing: **File example:** ```json { - "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat", + "path": "my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat", "size": 12345, "hash": "sha256:abcdef1234...", "ext": ".dat", @@ -261,7 +259,7 @@ The `object` type is stored as a `JSON` column in MySQL containing: **Folder example:** ```json { - "path": "my_schema/objects/Recording/subject_id=123/session_id=45/raw_data_pL9nR4wE", + "path": "my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_pL9nR4wE", "size": 567890, "hash": "sha256:fedcba9876...", "ext": null, @@ -314,8 +312,8 @@ Storage paths are **deterministically constructed** from record metadata, enabli 1. **Location** - from configuration (`object_storage.location`) 2. **Partition attributes** - promoted PK attributes (if `partition_pattern` configured) 3. **Schema name** - from the table's schema -4. **Object directory** - `objects/` -5. **Table name** - the table class name +4. **Table name** - the table class name +5. **Object directory** - `objects/` 6. **Primary key encoding** - remaining PK attributes and values 7. **Suffixed filename** - `{field}_{token}{ext}` @@ -323,14 +321,16 @@ Storage paths are **deterministically constructed** from record metadata, enabli **Without partitioning:** ``` -{location}/{schema}/objects/{Table}/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../{field}_{token}{ext} +{location}/{schema}/{Table}/objects/{pk_attr1}={pk_val1}/{pk_attr2}={pk_val2}/.../{field}_{token}{ext} ``` **With partitioning:** ``` -{location}/{partition_attr}={val}/.../schema/objects/{Table}/{remaining_pk_attrs}/.../{field}_{token}{ext} +{location}/{partition_attr}={val}/.../schema/{Table}/objects/{remaining_pk_attrs}/.../{field}_{token}{ext} ``` +Note: The `objects/` directory follows the table name, allowing each table folder to also contain tabular data exports (e.g., `data.parquet`) alongside the objects. + ### Partitioning The **partition pattern** allows promoting certain primary key attributes to the beginning of the path (after `location`). This organizes storage by high-level attributes like subject or experiment, enabling: @@ -364,7 +364,7 @@ class Recording(dj.Manual): Inserting `{"subject_id": 123, "session_id": 45, "raw_data": "/path/to/recording.dat"}` produces: ``` -my_project/my_schema/objects/Recording/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat +my_project/my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat ``` Note: The filename is `raw_data` (field name) with `.dat` extension (from source file). @@ -374,7 +374,7 @@ Note: The filename is `raw_data` (field name) with `.dat` extension (from source With `partition_pattern = "{subject_id}"`: ``` -my_project/subject_id=123/my_schema/objects/Recording/session_id=45/raw_data_Ax7bQ2kM.dat +my_project/subject_id=123/my_schema/Recording/objects/session_id=45/raw_data_Ax7bQ2kM.dat ``` The `subject_id` is promoted to the path root, grouping all files for subject 123 together regardless of schema or table. From 0ea880ae1f13e1a3ad1291c7f385b97dac2b8043 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:02:16 +0000 Subject: [PATCH 060/219] Make content hashing optional, add folder manifests - Hash is null by default to avoid performance overhead for large objects - Optional hash parameter on insert: hash="sha256", "md5", or "xxhash" - Staged inserts never compute hashes (no local copy to hash from) - Folders get a manifest file (.manifest.json) with file list and sizes - Manifest enables integrity verification without content hashing - Add ObjectRef.verify() method for integrity checking --- docs/src/design/tables/file-type-spec.md | 79 ++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 941f0790f..5ad2ff29e 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -245,6 +245,19 @@ The `object` type is stored as a `JSON` column in MySQL containing: **File example:** ```json +{ + "path": "my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat", + "size": 12345, + "hash": null, + "ext": ".dat", + "is_dir": false, + "timestamp": "2025-01-15T10:30:00Z", + "mime_type": "application/octet-stream" +} +``` + +**File with optional hash:** +```json { "path": "my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat", "size": 12345, @@ -261,7 +274,7 @@ The `object` type is stored as a `JSON` column in MySQL containing: { "path": "my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_pL9nR4wE", "size": 567890, - "hash": "sha256:fedcba9876...", + "hash": null, "ext": null, "is_dir": true, "timestamp": "2025-01-15T10:30:00Z", @@ -275,13 +288,59 @@ The `object` type is stored as a `JSON` column in MySQL containing: |-------|------|----------|-------------| | `path` | string | Yes | Full path/key within storage backend (includes token) | | `size` | integer | Yes | Total size in bytes (sum for folders) | -| `hash` | string | Yes | Content hash with algorithm prefix | +| `hash` | string/null | Yes | Content hash with algorithm prefix, or null (default) | | `ext` | string/null | Yes | File extension (e.g., `.dat`, `.zarr`) or null | | `is_dir` | boolean | Yes | True if stored content is a directory | | `timestamp` | string | Yes | ISO 8601 upload timestamp | | `mime_type` | string | No | MIME type (files only, auto-detected from extension) | | `item_count` | integer | No | Number of files (folders only) | +### Content Hashing + +By default, **no content hash is computed** to avoid performance overhead for large objects. Storage backend integrity is trusted. + +**Optional hashing** can be requested per-insert: + +```python +# Default - no hash (fast) +Recording.insert1({..., "raw_data": "/path/to/large.dat"}) + +# Request hash computation +Recording.insert1({..., "raw_data": "/path/to/important.dat"}, hash="sha256") +``` + +Supported hash algorithms: `sha256`, `md5`, `xxhash` (xxh3, faster for large files) + +**Staged inserts never compute hashes** - data is written directly to storage without a local copy to hash. + +### Folder Manifests + +For folders (directories), a **manifest file** is created alongside the folder to enable integrity verification without computing content hashes: + +``` +raw_data_pL9nR4wE/ +raw_data_pL9nR4wE.manifest.json +``` + +**Manifest content:** +```json +{ + "files": [ + {"path": "file1.dat", "size": 1234}, + {"path": "subdir/file2.dat", "size": 5678}, + {"path": "subdir/file3.dat", "size": 91011} + ], + "total_size": 567890, + "item_count": 42, + "created": "2025-01-15T10:30:00Z" +} +``` + +The manifest enables: +- Quick verification that all expected files exist +- Size validation without reading file contents +- Detection of missing or extra files + ### Filename Convention The stored filename is **always derived from the field name**: @@ -736,7 +795,7 @@ file_ref = record["raw_data"] # Access metadata (no I/O) print(file_ref.path) # Full storage path print(file_ref.size) # File size in bytes -print(file_ref.hash) # Content hash +print(file_ref.hash) # Content hash (if computed) or None print(file_ref.ext) # File extension (e.g., ".dat") or None print(file_ref.is_dir) # True if stored content is a folder @@ -840,7 +899,7 @@ class ObjectRef: path: str size: int - hash: str + hash: str | None # content hash (if computed) or None ext: str | None # file extension (e.g., ".dat") or None is_dir: bool timestamp: datetime @@ -875,6 +934,18 @@ class ObjectRef: # Common operations def download(self, destination: Path | str, subpath: str | None = None) -> Path: ... def exists(self, subpath: str | None = None) -> bool: ... + + # Integrity verification + def verify(self) -> bool: + """ + Verify object integrity. + + For files: checks size matches, and hash if available. + For folders: validates manifest (all files exist with correct sizes). + + Returns True if valid, raises IntegrityError with details if not. + """ + ... ``` #### fsspec Integration From c340ec7799a4340cbee3396e271fe54bc8dca173 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:05:45 +0000 Subject: [PATCH 061/219] Clarify folder manifest storage location and rationale --- docs/src/design/tables/file-type-spec.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 5ad2ff29e..e8f013691 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -315,7 +315,7 @@ Supported hash algorithms: `sha256`, `md5`, `xxhash` (xxh3, faster for large fil ### Folder Manifests -For folders (directories), a **manifest file** is created alongside the folder to enable integrity verification without computing content hashes: +For folders (directories), a **manifest file** is created alongside the folder in the object store to enable integrity verification without computing content hashes: ``` raw_data_pL9nR4wE/ @@ -336,6 +336,11 @@ raw_data_pL9nR4wE.manifest.json } ``` +**Design rationale:** +- Stored in object store (not database) to avoid bloating the JSON for folders with many files +- Placed alongside folder (not inside) to avoid polluting folder contents and interfering with tools like Zarr +- Enables self-contained verification without database access + The manifest enables: - Quick verification that all expected files exist - Size validation without reading file contents From 6cd9b9cef79ca6aea10305e3a4e1ac72083bd638 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:17:11 +0000 Subject: [PATCH 062/219] Add optional database_host and database_name to store metadata - Enables bidirectional mapping between object stores and databases - Fields are informational only, not enforced at runtime - Alternative: admins ensure unique project_name across namespace - Managed platforms may handle this mapping externally --- docs/src/design/tables/file-type-spec.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index e8f013691..5109cda18 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -170,7 +170,9 @@ s3://bucket/my_project/datajoint_store.json "project_name": "my_project", "created": "2025-01-15T10:30:00Z", "format_version": "1.0", - "datajoint_version": "0.15.0" + "datajoint_version": "0.15.0", + "database_host": "db.example.com", + "database_name": "my_project_db" } ``` @@ -182,6 +184,10 @@ s3://bucket/my_project/datajoint_store.json | `created` | string | Yes | ISO 8601 timestamp of store creation | | `format_version` | string | Yes | Store format version for compatibility | | `datajoint_version` | string | Yes | DataJoint version that created the store | +| `database_host` | string | No | Database server hostname (for bidirectional mapping) | +| `database_name` | string | No | Database name (for bidirectional mapping) | + +The optional `database_host` and `database_name` fields enable bidirectional mapping between object stores and databases. This is informational only - not enforced at runtime. Administrators can alternatively ensure unique `project_name` values across their namespace, and managed platforms may handle this mapping externally. ### Store Initialization From 38844f12ddca9d67f199e8390203e4ef55de72bf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:22:57 +0000 Subject: [PATCH 063/219] Highlight no hidden tables - key architectural difference - Legacy attach@store and filepath@store use hidden ~external_* tables - New object type stores all metadata inline in JSON column - Benefits: simpler schema, self-contained records, easier debugging - No reference counting complexity --- docs/src/design/tables/file-type-spec.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 5109cda18..dc1eae987 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -1003,11 +1003,28 @@ azure = ["adlfs"] | Store config | Per-attribute | Per-attribute | Per-pipeline | | Path control | DataJoint | User-managed | DataJoint | | DB column | binary(16) UUID | binary(16) UUID | JSON | +| Hidden tables | Yes (external) | Yes (external) | **No** | | Backend | File/S3 only | File/S3 only | fsspec (any) | | Partitioning | Hash-based | User path | Configurable | -| Metadata | External table | External table | Inline JSON | +| Metadata storage | External table | External table | Inline JSON | | Deduplication | By content | By path | None | +### No Hidden Tables + +A key architectural difference: the `object` type does **not** use hidden external tables. + +The legacy `attach@store` and `filepath@store` types store a UUID in the table column and maintain a separate hidden `~external_*` table containing: +- File paths/keys +- Checksums +- Size information +- Reference counts + +The `object` type eliminates this complexity by storing all metadata **inline** in the JSON column. This provides: +- **Simpler schema** - no hidden tables to manage or migrate +- **Self-contained records** - all information in one place +- **Easier debugging** - metadata visible directly in queries +- **No reference counting** - each record owns its object exclusively + ### Legacy Type Deprecation The existing `attach@store` and `filepath@store` types will be: From d65ece72efc4a2137edf30d43467540018cd5f9f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:34:43 +0000 Subject: [PATCH 064/219] Refactor external storage to use fsspec for unified backend - Add fsspec>=2023.1.0 as core dependency - Add optional dependencies for cloud backends (s3fs, gcsfs, adlfs) - Create new storage.py module with StorageBackend class - Unified interface for file, S3, GCS, and Azure storage - Methods: put_file, get_file, put_buffer, get_buffer, exists, remove - Refactor ExternalTable to use StorageBackend instead of protocol-specific code - Replace _upload_file, _download_file, etc. with storage backend calls - Add storage property, deprecate s3 property - Update settings.py to support GCS and Azure protocols - Add deprecation warning to s3.py Folder class - Module kept for backward compatibility - Will be removed in future version This lays the foundation for the new object type which will also use fsspec. --- pyproject.toml | 5 + src/datajoint/external.py | 91 ++++++------ src/datajoint/s3.py | 23 ++- src/datajoint/settings.py | 29 +++- src/datajoint/storage.py | 286 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 381 insertions(+), 53 deletions(-) create mode 100644 src/datajoint/storage.py diff --git a/pyproject.toml b/pyproject.toml index dc151d7cf..8d27481eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,12 +17,14 @@ dependencies = [ "networkx", "pydot", "minio>=7.0.0", + "fsspec>=2023.1.0", "matplotlib", "faker", "urllib3", "setuptools", "pydantic-settings>=2.0.0", ] + requires-python = ">=3.10,<3.14" authors = [ {name = "Dimitri Yatsenko", email = "dimitri@datajoint.com"}, @@ -90,6 +92,9 @@ test = [ ] [project.optional-dependencies] +s3 = ["s3fs>=2023.1.0"] +gcs = ["gcsfs>=2023.1.0"] +azure = ["adlfs>=2023.1.0"] dev = [ "pre-commit", "ruff", diff --git a/src/datajoint/external.py b/src/datajoint/external.py index 3f9efcf8e..c41086d05 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -1,15 +1,17 @@ import logging +import warnings from collections.abc import Mapping from pathlib import Path, PurePosixPath, PureWindowsPath from tqdm import tqdm -from . import errors, s3 +from . import errors from .declare import EXTERNAL_TABLE_ROOT from .errors import DataJointError, MissingExternalFile from .hash import uuid_from_buffer, uuid_from_file from .heading import Heading from .settings import config +from .storage import StorageBackend from .table import FreeTable, Table from .utils import safe_copy, safe_write @@ -38,7 +40,7 @@ class ExternalTable(Table): def __init__(self, connection, store, database): self.store = store self.spec = config.get_store_spec(store) - self._s3 = None + self._storage = None self.database = database self._connection = connection self._heading = Heading( @@ -52,9 +54,8 @@ def __init__(self, connection, store, database): self._support = [self.full_table_name] if not self.is_declared: self.declare() - self._s3 = None - if self.spec["protocol"] == "file" and not Path(self.spec["location"]).is_dir(): - raise FileNotFoundError("Inaccessible local directory %s" % self.spec["location"]) from None + # Initialize storage backend (validates configuration) + _ = self.storage @property def definition(self): @@ -73,17 +74,32 @@ def definition(self): def table_name(self): return f"{EXTERNAL_TABLE_ROOT}_{self.store}" + @property + def storage(self) -> StorageBackend: + """Get or create the storage backend instance.""" + if self._storage is None: + self._storage = StorageBackend(self.spec) + return self._storage + @property def s3(self): - if self._s3 is None: - self._s3 = s3.Folder(**self.spec) - return self._s3 + """Deprecated: Use storage property instead.""" + warnings.warn( + "ExternalTable.s3 is deprecated. Use ExternalTable.storage instead.", + DeprecationWarning, + stacklevel=2, + ) + # For backward compatibility, return a legacy s3.Folder if needed + from . import s3 + if not hasattr(self, "_s3_legacy") or self._s3_legacy is None: + self._s3_legacy = s3.Folder(**self.spec) + return self._s3_legacy # - low-level operations - private def _make_external_filepath(self, relative_filepath): """resolve the complete external path based on the relative path""" - # Strip root + # Strip root for S3 paths if self.spec["protocol"] == "s3": posix_path = PurePosixPath(PureWindowsPath(self.spec["location"])) location_path = ( @@ -92,11 +108,13 @@ def _make_external_filepath(self, relative_filepath): else Path(posix_path) ) return PurePosixPath(location_path, relative_filepath) - # Preserve root + # Preserve root for local filesystem elif self.spec["protocol"] == "file": return PurePosixPath(Path(self.spec["location"]), relative_filepath) else: - assert False + # For other protocols (gcs, azure, etc.), treat like S3 + location = self.spec.get("location", "") + return PurePosixPath(location, relative_filepath) if location else PurePosixPath(relative_filepath) def _make_uuid_path(self, uuid, suffix=""): """create external path based on the uuid hash""" @@ -109,57 +127,32 @@ def _make_uuid_path(self, uuid, suffix=""): ) def _upload_file(self, local_path, external_path, metadata=None): - if self.spec["protocol"] == "s3": - self.s3.fput(local_path, external_path, metadata) - elif self.spec["protocol"] == "file": - safe_copy(local_path, external_path, overwrite=True) - else: - assert False + """Upload a file to external storage using fsspec backend.""" + self.storage.put_file(local_path, external_path, metadata) def _download_file(self, external_path, download_path): - if self.spec["protocol"] == "s3": - self.s3.fget(external_path, download_path) - elif self.spec["protocol"] == "file": - safe_copy(external_path, download_path) - else: - assert False + """Download a file from external storage using fsspec backend.""" + self.storage.get_file(external_path, download_path) def _upload_buffer(self, buffer, external_path): - if self.spec["protocol"] == "s3": - self.s3.put(external_path, buffer) - elif self.spec["protocol"] == "file": - safe_write(external_path, buffer) - else: - assert False + """Upload bytes to external storage using fsspec backend.""" + self.storage.put_buffer(buffer, external_path) def _download_buffer(self, external_path): - if self.spec["protocol"] == "s3": - return self.s3.get(external_path) - if self.spec["protocol"] == "file": - try: - return Path(external_path).read_bytes() - except FileNotFoundError: - raise errors.MissingExternalFile(f"Missing external file {external_path}") from None - assert False + """Download bytes from external storage using fsspec backend.""" + return self.storage.get_buffer(external_path) def _remove_external_file(self, external_path): - if self.spec["protocol"] == "s3": - self.s3.remove_object(external_path) - elif self.spec["protocol"] == "file": - try: - Path(external_path).unlink() - except FileNotFoundError: - pass + """Remove a file from external storage using fsspec backend.""" + self.storage.remove(external_path) def exists(self, external_filepath): """ + Check if an external file is accessible using fsspec backend. + :return: True if the external file is accessible """ - if self.spec["protocol"] == "s3": - return self.s3.exists(external_filepath) - if self.spec["protocol"] == "file": - return Path(external_filepath).is_file() - assert False + return self.storage.exists(external_filepath) # --- BLOBS ---- diff --git a/src/datajoint/s3.py b/src/datajoint/s3.py index e107a7f4b..2e2ea151a 100644 --- a/src/datajoint/s3.py +++ b/src/datajoint/s3.py @@ -1,9 +1,19 @@ """ -AWS S3 operations +AWS S3 operations using minio client. + +.. deprecated:: 0.15.0 + This module is deprecated. Use :mod:`datajoint.storage` with fsspec backend instead. + The minio-based S3 client will be removed in a future version. + + Migration guide: + - Instead of importing from datajoint.s3, use datajoint.storage.StorageBackend + - StorageBackend provides a unified interface for all storage protocols + - See datajoint.storage module for details """ import logging import uuid +import warnings from io import BytesIO from pathlib import Path @@ -17,7 +27,10 @@ class Folder: """ - A Folder instance manipulates a flat folder of objects within an S3-compatible object store + A Folder instance manipulates a flat folder of objects within an S3-compatible object store. + + .. deprecated:: 0.15.0 + Use :class:`datajoint.storage.StorageBackend` instead. """ def __init__( @@ -31,6 +44,12 @@ def __init__( proxy_server=None, **_, ): + warnings.warn( + "datajoint.s3.Folder is deprecated and will be removed in a future version. " + "Use datajoint.storage.StorageBackend with fsspec instead.", + DeprecationWarning, + stacklevel=2, + ) # from https://docs.min.io/docs/python-client-api-reference self.client = minio.Minio( endpoint, diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 65b91aa2c..308b0452d 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -275,13 +275,19 @@ def get_store_spec(self, store: str) -> dict[str, Any]: # Validate protocol protocol = spec.get("protocol", "").lower() - if protocol not in ("file", "s3"): - raise DataJointError(f'Missing or invalid protocol in config.stores["{store}"]') + supported_protocols = ("file", "s3", "gcs", "azure") + if protocol not in supported_protocols: + raise DataJointError( + f'Missing or invalid protocol in config.stores["{store}"]. ' + f'Supported protocols: {", ".join(supported_protocols)}' + ) # Define required and allowed keys by protocol required_keys: dict[str, tuple[str, ...]] = { "file": ("protocol", "location"), "s3": ("protocol", "endpoint", "bucket", "access_key", "secret_key", "location"), + "gcs": ("protocol", "bucket", "location"), + "azure": ("protocol", "container", "location"), } allowed_keys: dict[str, tuple[str, ...]] = { "file": ("protocol", "location", "subfolding", "stage"), @@ -297,6 +303,25 @@ def get_store_spec(self, store: str) -> dict[str, Any]: "stage", "proxy_server", ), + "gcs": ( + "protocol", + "bucket", + "location", + "token", + "project", + "subfolding", + "stage", + ), + "azure": ( + "protocol", + "container", + "location", + "account_name", + "account_key", + "connection_string", + "subfolding", + "stage", + ), } # Check required keys diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py new file mode 100644 index 000000000..cb3dada5b --- /dev/null +++ b/src/datajoint/storage.py @@ -0,0 +1,286 @@ +""" +Storage backend abstraction using fsspec for unified file operations. + +This module provides a unified interface for storage operations across different +backends (local filesystem, S3, GCS, Azure, etc.) using the fsspec library. +""" + +import logging +from io import BytesIO +from pathlib import Path, PurePosixPath +from typing import Any + +import fsspec + +from . import errors + +logger = logging.getLogger(__name__.split(".")[0]) + + +class StorageBackend: + """ + Unified storage backend using fsspec. + + Provides a consistent interface for file operations across different storage + backends including local filesystem and cloud object storage (S3, GCS, Azure). + """ + + def __init__(self, spec: dict[str, Any]): + """ + Initialize storage backend from configuration spec. + + Args: + spec: Storage configuration dictionary containing: + - protocol: Storage protocol ('file', 's3', 'gcs', 'azure') + - location: Base path or bucket prefix + - bucket: Bucket name (for cloud storage) + - endpoint: Endpoint URL (for S3-compatible storage) + - access_key: Access key (for cloud storage) + - secret_key: Secret key (for cloud storage) + - secure: Use HTTPS (default: True for cloud) + - Additional protocol-specific options + """ + self.spec = spec + self.protocol = spec.get("protocol", "file") + self._fs = None + self._validate_spec() + + def _validate_spec(self): + """Validate configuration spec for the protocol.""" + if self.protocol == "file": + location = self.spec.get("location") + if location and not Path(location).is_dir(): + raise FileNotFoundError(f"Inaccessible local directory {location}") + elif self.protocol == "s3": + required = ["endpoint", "bucket", "access_key", "secret_key"] + missing = [k for k in required if not self.spec.get(k)] + if missing: + raise errors.DataJointError(f"Missing S3 configuration: {', '.join(missing)}") + + @property + def fs(self) -> fsspec.AbstractFileSystem: + """Get or create the fsspec filesystem instance.""" + if self._fs is None: + self._fs = self._create_filesystem() + return self._fs + + def _create_filesystem(self) -> fsspec.AbstractFileSystem: + """Create fsspec filesystem based on protocol.""" + if self.protocol == "file": + return fsspec.filesystem("file") + + elif self.protocol == "s3": + # Build S3 configuration + endpoint = self.spec["endpoint"] + # Determine if endpoint includes protocol + if not endpoint.startswith(("http://", "https://")): + secure = self.spec.get("secure", False) + endpoint_url = f"{'https' if secure else 'http'}://{endpoint}" + else: + endpoint_url = endpoint + + return fsspec.filesystem( + "s3", + key=self.spec["access_key"], + secret=self.spec["secret_key"], + client_kwargs={"endpoint_url": endpoint_url}, + ) + + elif self.protocol == "gcs": + return fsspec.filesystem( + "gcs", + token=self.spec.get("token"), + project=self.spec.get("project"), + ) + + elif self.protocol == "azure": + return fsspec.filesystem( + "abfs", + account_name=self.spec.get("account_name"), + account_key=self.spec.get("account_key"), + connection_string=self.spec.get("connection_string"), + ) + + else: + raise errors.DataJointError(f"Unsupported storage protocol: {self.protocol}") + + def _full_path(self, path: str | PurePosixPath) -> str: + """ + Construct full path including bucket for cloud storage. + + Args: + path: Relative path within the storage location + + Returns: + Full path suitable for fsspec operations + """ + path = str(path) + if self.protocol == "s3": + bucket = self.spec["bucket"] + return f"{bucket}/{path}" + elif self.protocol in ("gcs", "azure"): + bucket = self.spec.get("bucket") or self.spec.get("container") + return f"{bucket}/{path}" + else: + # Local filesystem - path is already absolute or relative to cwd + return path + + def put_file(self, local_path: str | Path, remote_path: str | PurePosixPath, metadata: dict | None = None): + """ + Upload a file from local filesystem to storage. + + Args: + local_path: Path to local file + remote_path: Destination path in storage + metadata: Optional metadata to attach to the file + """ + full_path = self._full_path(remote_path) + logger.debug(f"put_file: {local_path} -> {self.protocol}:{full_path}") + + if self.protocol == "file": + # For local filesystem, use safe copy with atomic rename + from .utils import safe_copy + Path(full_path).parent.mkdir(parents=True, exist_ok=True) + safe_copy(local_path, full_path, overwrite=True) + else: + # For cloud storage, use fsspec put + self.fs.put_file(str(local_path), full_path) + + def get_file(self, remote_path: str | PurePosixPath, local_path: str | Path): + """ + Download a file from storage to local filesystem. + + Args: + remote_path: Path in storage + local_path: Destination path on local filesystem + """ + full_path = self._full_path(remote_path) + logger.debug(f"get_file: {self.protocol}:{full_path} -> {local_path}") + + local_path = Path(local_path) + local_path.parent.mkdir(parents=True, exist_ok=True) + + if self.protocol == "file": + from .utils import safe_copy + safe_copy(full_path, local_path) + else: + self.fs.get_file(full_path, str(local_path)) + + def put_buffer(self, buffer: bytes, remote_path: str | PurePosixPath): + """ + Write bytes to storage. + + Args: + buffer: Bytes to write + remote_path: Destination path in storage + """ + full_path = self._full_path(remote_path) + logger.debug(f"put_buffer: {len(buffer)} bytes -> {self.protocol}:{full_path}") + + if self.protocol == "file": + from .utils import safe_write + Path(full_path).parent.mkdir(parents=True, exist_ok=True) + safe_write(full_path, buffer) + else: + self.fs.pipe_file(full_path, buffer) + + def get_buffer(self, remote_path: str | PurePosixPath) -> bytes: + """ + Read bytes from storage. + + Args: + remote_path: Path in storage + + Returns: + File contents as bytes + """ + full_path = self._full_path(remote_path) + logger.debug(f"get_buffer: {self.protocol}:{full_path}") + + try: + if self.protocol == "file": + return Path(full_path).read_bytes() + else: + return self.fs.cat_file(full_path) + except FileNotFoundError: + raise errors.MissingExternalFile(f"Missing external file {full_path}") from None + + def exists(self, remote_path: str | PurePosixPath) -> bool: + """ + Check if a file exists in storage. + + Args: + remote_path: Path in storage + + Returns: + True if file exists + """ + full_path = self._full_path(remote_path) + logger.debug(f"exists: {self.protocol}:{full_path}") + + if self.protocol == "file": + return Path(full_path).is_file() + else: + return self.fs.exists(full_path) + + def remove(self, remote_path: str | PurePosixPath): + """ + Remove a file from storage. + + Args: + remote_path: Path in storage + """ + full_path = self._full_path(remote_path) + logger.debug(f"remove: {self.protocol}:{full_path}") + + try: + if self.protocol == "file": + Path(full_path).unlink(missing_ok=True) + else: + self.fs.rm(full_path) + except FileNotFoundError: + pass # Already gone + + def size(self, remote_path: str | PurePosixPath) -> int: + """ + Get file size in bytes. + + Args: + remote_path: Path in storage + + Returns: + File size in bytes + """ + full_path = self._full_path(remote_path) + + if self.protocol == "file": + return Path(full_path).stat().st_size + else: + return self.fs.size(full_path) + + def open(self, remote_path: str | PurePosixPath, mode: str = "rb"): + """ + Open a file in storage. + + Args: + remote_path: Path in storage + mode: File mode ('rb', 'wb', etc.) + + Returns: + File-like object + """ + full_path = self._full_path(remote_path) + return self.fs.open(full_path, mode) + + +def get_storage_backend(spec: dict[str, Any]) -> StorageBackend: + """ + Factory function to create a storage backend from configuration. + + Args: + spec: Storage configuration dictionary + + Returns: + StorageBackend instance + """ + return StorageBackend(spec) From 4b7e7bd75303bbf2f65457bb013a93257e117001 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:48:32 +0000 Subject: [PATCH 065/219] Fix unused imports (ruff lint) --- src/datajoint/external.py | 3 +-- src/datajoint/storage.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/datajoint/external.py b/src/datajoint/external.py index c41086d05..c03d0a16f 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -5,7 +5,6 @@ from tqdm import tqdm -from . import errors from .declare import EXTERNAL_TABLE_ROOT from .errors import DataJointError, MissingExternalFile from .hash import uuid_from_buffer, uuid_from_file @@ -13,7 +12,7 @@ from .settings import config from .storage import StorageBackend from .table import FreeTable, Table -from .utils import safe_copy, safe_write +from .utils import safe_write logger = logging.getLogger(__name__.split(".")[0]) diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index cb3dada5b..7812cb56c 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -6,7 +6,6 @@ """ import logging -from io import BytesIO from pathlib import Path, PurePosixPath from typing import Any From 949b8a6f09a8439cfe9e59901cdec3647b4c1978 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 00:51:27 +0000 Subject: [PATCH 066/219] Fix ruff-format: add blank lines after local imports --- src/datajoint/external.py | 1 + src/datajoint/storage.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/datajoint/external.py b/src/datajoint/external.py index c03d0a16f..dbb99cae7 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -90,6 +90,7 @@ def s3(self): ) # For backward compatibility, return a legacy s3.Folder if needed from . import s3 + if not hasattr(self, "_s3_legacy") or self._s3_legacy is None: self._s3_legacy = s3.Folder(**self.spec) return self._s3_legacy diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index 7812cb56c..903bdc0d6 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -139,6 +139,7 @@ def put_file(self, local_path: str | Path, remote_path: str | PurePosixPath, met if self.protocol == "file": # For local filesystem, use safe copy with atomic rename from .utils import safe_copy + Path(full_path).parent.mkdir(parents=True, exist_ok=True) safe_copy(local_path, full_path, overwrite=True) else: @@ -161,6 +162,7 @@ def get_file(self, remote_path: str | PurePosixPath, local_path: str | Path): if self.protocol == "file": from .utils import safe_copy + safe_copy(full_path, local_path) else: self.fs.get_file(full_path, str(local_path)) @@ -178,6 +180,7 @@ def put_buffer(self, buffer: bytes, remote_path: str | PurePosixPath): if self.protocol == "file": from .utils import safe_write + Path(full_path).parent.mkdir(parents=True, exist_ok=True) safe_write(full_path, buffer) else: From 0019109475d3ce57baf7ffbbec1fe5a45aabafab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:07:46 +0000 Subject: [PATCH 067/219] Implement object column type for managed file storage This commit adds a new `object` column type that provides managed file/folder storage with fsspec backend integration. Key features: - Object type declaration in declare.py (stores as JSON in MySQL) - ObjectRef class for fetch behavior with fsspec accessors (.fs, .store, .full_path) - Insert processing for file paths, folder paths, and (ext, stream) tuples - staged_insert1 context manager for direct writes (Zarr/xarray compatibility) - Path generation with partition pattern support - Store metadata file (datajoint_store.json) verification/creation - Folder manifest files for integrity verification The object type stores metadata inline (no hidden tables), supports multiple storage backends via fsspec (file, S3, GCS, Azure), and provides ObjectRef handles on fetch with direct storage access. --- src/datajoint/__init__.py | 2 + src/datajoint/declare.py | 5 + src/datajoint/fetch.py | 11 + src/datajoint/heading.py | 8 +- src/datajoint/objectref.py | 357 +++++++++++++++++++++++++++++++++ src/datajoint/settings.py | 95 +++++++++ src/datajoint/staged_insert.py | 316 +++++++++++++++++++++++++++++ src/datajoint/storage.py | 290 ++++++++++++++++++++++++++ src/datajoint/table.py | 178 +++++++++++++++- 9 files changed, 1256 insertions(+), 6 deletions(-) create mode 100644 src/datajoint/objectref.py create mode 100644 src/datajoint/staged_insert.py diff --git a/src/datajoint/__init__.py b/src/datajoint/__init__.py index 0f8123c66..2fba6bd84 100644 --- a/src/datajoint/__init__.py +++ b/src/datajoint/__init__.py @@ -52,6 +52,7 @@ "key_hash", "logger", "cli", + "ObjectRef", ] from . import errors @@ -66,6 +67,7 @@ from .fetch import key from .hash import key_hash from .logging import logger +from .objectref import ObjectRef from .schemas import Schema, VirtualModule, list_schemas from .settings import config from .table import FreeTable, Table diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index c1a22f0ca..8ad58b33d 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -64,6 +64,7 @@ INTERNAL_ATTACH=r"attach$", EXTERNAL_ATTACH=r"attach@(?P[a-z][\-\w]*)$", FILEPATH=r"filepath@(?P[a-z][\-\w]*)$", + OBJECT=r"object$", # managed object storage (files/folders) UUID=r"uuid$", ADAPTED=r"<.+>$", ).items() @@ -76,6 +77,7 @@ "EXTERNAL_ATTACH", "EXTERNAL_BLOB", "FILEPATH", + "OBJECT", "ADAPTED", } | set(TYPE_ALIASES) NATIVE_TYPES = set(TYPE_PATTERN) - SPECIAL_TYPES @@ -464,6 +466,9 @@ def substitute_special_type(match, category, foreign_key_sql, context): match["type"] = UUID_DATA_TYPE elif category == "INTERNAL_ATTACH": match["type"] = "LONGBLOB" + elif category == "OBJECT": + # Object type stores metadata as JSON - no foreign key to external table + match["type"] = "JSON" elif category in EXTERNAL_TYPES: if category == "FILEPATH" and not _support_filepath_types(): raise DataJointError( diff --git a/src/datajoint/fetch.py b/src/datajoint/fetch.py index 5d02b52b0..3ada0fc61 100644 --- a/src/datajoint/fetch.py +++ b/src/datajoint/fetch.py @@ -12,7 +12,9 @@ from . import blob, hash from .errors import DataJointError +from .objectref import ObjectRef from .settings import config +from .storage import StorageBackend from .utils import safe_write @@ -48,6 +50,15 @@ def _get(connection, attr, data, squeeze, download_path): """ if data is None: return + if attr.is_object: + # Object type - return ObjectRef handle + json_data = json.loads(data) if isinstance(data, str) else data + try: + spec = config.get_object_storage_spec() + backend = StorageBackend(spec) + except DataJointError: + backend = None + return ObjectRef.from_json(json_data, backend=backend) if attr.json: return json.loads(data) diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index 45e35998c..1cc66afde 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -32,6 +32,7 @@ is_blob=False, is_attachment=False, is_filepath=False, + is_object=False, is_external=False, is_hidden=False, adapter=None, @@ -136,7 +137,7 @@ def blobs(self): @property def non_blobs(self): - return [k for k, v in self.attributes.items() if not (v.is_blob or v.is_attachment or v.is_filepath or v.json)] + return [k for k, v in self.attributes.items() if not (v.is_blob or v.is_attachment or v.is_filepath or v.is_object or v.json)] @property def new_attributes(self): @@ -262,6 +263,7 @@ def _init_from_database(self): json=bool(TYPE_PATTERN["JSON"].match(attr["type"])), is_attachment=False, is_filepath=False, + is_object=False, adapter=None, store=None, is_external=False, @@ -325,6 +327,7 @@ def _init_from_database(self): unsupported=False, is_attachment=category in ("INTERNAL_ATTACH", "EXTERNAL_ATTACH"), is_filepath=category == "FILEPATH", + is_object=category == "OBJECT", # INTERNAL_BLOB is not a custom type but is included for completeness is_blob=category in ("INTERNAL_BLOB", "EXTERNAL_BLOB"), uuid=category == "UUID", @@ -337,10 +340,11 @@ def _init_from_database(self): attr["is_blob"], attr["is_attachment"], attr["is_filepath"], + attr["is_object"], attr["json"], ) ): - raise DataJointError("Json, Blob, attachment, or filepath attributes are not allowed in the primary key") + raise DataJointError("Json, Blob, attachment, filepath, or object attributes are not allowed in the primary key") if attr["string"] and attr["default"] is not None and attr["default"] not in sql_literals: attr["default"] = '"%s"' % attr["default"] diff --git a/src/datajoint/objectref.py b/src/datajoint/objectref.py new file mode 100644 index 000000000..8707e060f --- /dev/null +++ b/src/datajoint/objectref.py @@ -0,0 +1,357 @@ +""" +ObjectRef class for handling fetched object type attributes. + +This module provides the ObjectRef class which represents a reference to a file +or folder stored in the pipeline's object storage backend. It provides metadata +access and direct fsspec-based file operations. +""" + +import json +import mimetypes +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import IO, Any, Iterator + +import fsspec + +from .errors import DataJointError +from .storage import StorageBackend + + +class IntegrityError(DataJointError): + """Raised when object integrity verification fails.""" + + pass + + +@dataclass +class ObjectRef: + """ + Handle to a file or folder stored in the pipeline's object storage backend. + + This class is returned when fetching object-type attributes. It provides + metadata access without I/O, and methods for reading content directly + from the storage backend. + + Attributes: + path: Full path/key within storage backend (includes token) + size: Total size in bytes (sum for folders) + hash: Content hash with algorithm prefix, or None if not computed + ext: File extension (e.g., ".dat", ".zarr") or None + is_dir: True if stored content is a directory + timestamp: ISO 8601 upload timestamp + mime_type: MIME type (files only, auto-detected from extension) + item_count: Number of files (folders only) + """ + + path: str + size: int + hash: str | None + ext: str | None + is_dir: bool + timestamp: datetime + mime_type: str | None = None + item_count: int | None = None + _backend: StorageBackend | None = None + + @classmethod + def from_json(cls, json_data: dict | str, backend: StorageBackend | None = None) -> "ObjectRef": + """ + Create an ObjectRef from JSON metadata stored in the database. + + Args: + json_data: JSON string or dict containing object metadata + backend: StorageBackend instance for file operations + + Returns: + ObjectRef instance + """ + if isinstance(json_data, str): + data = json.loads(json_data) + else: + data = json_data + + timestamp = data.get("timestamp") + if isinstance(timestamp, str): + timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) + + return cls( + path=data["path"], + size=data["size"], + hash=data.get("hash"), + ext=data.get("ext"), + is_dir=data.get("is_dir", False), + timestamp=timestamp, + mime_type=data.get("mime_type"), + item_count=data.get("item_count"), + _backend=backend, + ) + + def to_json(self) -> dict: + """ + Convert ObjectRef to JSON-serializable dict for database storage. + + Returns: + Dict suitable for JSON serialization + """ + data = { + "path": self.path, + "size": self.size, + "hash": self.hash, + "ext": self.ext, + "is_dir": self.is_dir, + "timestamp": self.timestamp.isoformat() if self.timestamp else None, + } + if self.mime_type: + data["mime_type"] = self.mime_type + if self.item_count is not None: + data["item_count"] = self.item_count + return data + + def _ensure_backend(self): + """Ensure storage backend is available for I/O operations.""" + if self._backend is None: + raise DataJointError( + "ObjectRef has no storage backend configured. " + "This usually means the object was created without a connection context." + ) + + @property + def fs(self) -> fsspec.AbstractFileSystem: + """ + Return fsspec filesystem for direct access. + + This allows integration with libraries like Zarr and xarray that + work with fsspec filesystems. + """ + self._ensure_backend() + return self._backend.fs + + @property + def store(self) -> fsspec.FSMap: + """ + Return FSMap suitable for Zarr/xarray. + + This provides a dict-like interface to the storage location, + compatible with zarr.open() and xarray.open_zarr(). + """ + self._ensure_backend() + full_path = self._backend._full_path(self.path) + return fsspec.FSMap(full_path, self._backend.fs) + + @property + def full_path(self) -> str: + """ + Return full URI (e.g., 's3://bucket/path'). + + This is the complete path including protocol and bucket/location. + """ + self._ensure_backend() + protocol = self._backend.protocol + if protocol == "file": + return str(Path(self._backend.spec.get("location", "")) / self.path) + elif protocol == "s3": + bucket = self._backend.spec["bucket"] + return f"s3://{bucket}/{self.path}" + elif protocol == "gcs": + bucket = self._backend.spec["bucket"] + return f"gs://{bucket}/{self.path}" + elif protocol == "azure": + container = self._backend.spec["container"] + return f"az://{container}/{self.path}" + else: + return self.path + + def read(self) -> bytes: + """ + Read entire file content as bytes. + + Returns: + File contents as bytes + + Raises: + DataJointError: If object is a directory + """ + if self.is_dir: + raise DataJointError("Cannot read() a directory. Use listdir() or walk() instead.") + self._ensure_backend() + return self._backend.get_buffer(self.path) + + def open(self, subpath: str | None = None, mode: str = "rb") -> IO: + """ + Open file for reading. + + Args: + subpath: Optional path within directory (for folder objects) + mode: File mode ('rb' for binary read, 'r' for text) + + Returns: + File-like object + """ + self._ensure_backend() + path = self.path + if subpath: + if not self.is_dir: + raise DataJointError("Cannot use subpath on a file object") + path = f"{self.path}/{subpath}" + return self._backend.open(path, mode) + + def listdir(self, subpath: str = "") -> list[str]: + """ + List contents of directory. + + Args: + subpath: Optional subdirectory path + + Returns: + List of filenames/directory names + """ + if not self.is_dir: + raise DataJointError("Cannot listdir() on a file. Use read() or open() instead.") + self._ensure_backend() + path = f"{self.path}/{subpath}" if subpath else self.path + full_path = self._backend._full_path(path) + entries = self._backend.fs.ls(full_path, detail=False) + # Return just the basename of each entry + return [e.split("/")[-1] for e in entries] + + def walk(self) -> Iterator[tuple[str, list[str], list[str]]]: + """ + Walk directory tree, similar to os.walk(). + + Yields: + Tuples of (dirpath, dirnames, filenames) + """ + if not self.is_dir: + raise DataJointError("Cannot walk() on a file.") + self._ensure_backend() + full_path = self._backend._full_path(self.path) + for root, dirs, files in self._backend.fs.walk(full_path): + # Make paths relative to the object root + rel_root = root[len(full_path) :].lstrip("/") + yield rel_root, dirs, files + + def download(self, destination: Path | str, subpath: str | None = None) -> Path: + """ + Download object to local filesystem. + + Args: + destination: Local directory or file path + subpath: Optional path within directory (for folder objects) + + Returns: + Path to downloaded file/directory + """ + self._ensure_backend() + destination = Path(destination) + + if subpath: + if not self.is_dir: + raise DataJointError("Cannot use subpath on a file object") + remote_path = f"{self.path}/{subpath}" + else: + remote_path = self.path + + if self.is_dir and not subpath: + # Download entire directory + destination.mkdir(parents=True, exist_ok=True) + full_path = self._backend._full_path(remote_path) + self._backend.fs.get(full_path, str(destination), recursive=True) + else: + # Download single file + if destination.is_dir(): + filename = remote_path.split("/")[-1] + destination = destination / filename + destination.parent.mkdir(parents=True, exist_ok=True) + self._backend.get_file(remote_path, destination) + + return destination + + def exists(self, subpath: str | None = None) -> bool: + """ + Check if object (or subpath within it) exists. + + Args: + subpath: Optional path within directory + + Returns: + True if exists + """ + self._ensure_backend() + path = f"{self.path}/{subpath}" if subpath else self.path + return self._backend.exists(path) + + def verify(self) -> bool: + """ + Verify object integrity. + + For files: checks size matches, and hash if available. + For folders: validates manifest (all files exist with correct sizes). + + Returns: + True if valid + + Raises: + IntegrityError: If verification fails with details + """ + self._ensure_backend() + + if self.is_dir: + return self._verify_folder() + else: + return self._verify_file() + + def _verify_file(self) -> bool: + """Verify a single file.""" + # Check existence + if not self._backend.exists(self.path): + raise IntegrityError(f"File does not exist: {self.path}") + + # Check size + actual_size = self._backend.size(self.path) + if actual_size != self.size: + raise IntegrityError(f"Size mismatch for {self.path}: expected {self.size}, got {actual_size}") + + # Check hash if available + if self.hash: + # TODO: Implement hash verification + pass + + return True + + def _verify_folder(self) -> bool: + """Verify a folder using its manifest.""" + manifest_path = f"{self.path}.manifest.json" + + if not self._backend.exists(manifest_path): + raise IntegrityError(f"Manifest file missing: {manifest_path}") + + # Read manifest + manifest_data = self._backend.get_buffer(manifest_path) + manifest = json.loads(manifest_data) + + # Verify each file in manifest + errors = [] + for file_info in manifest.get("files", []): + file_path = f"{self.path}/{file_info['path']}" + expected_size = file_info["size"] + + if not self._backend.exists(file_path): + errors.append(f"Missing file: {file_info['path']}") + else: + actual_size = self._backend.size(file_path) + if actual_size != expected_size: + errors.append(f"Size mismatch for {file_info['path']}: expected {expected_size}, got {actual_size}") + + if errors: + raise IntegrityError(f"Folder verification failed:\n" + "\n".join(errors)) + + return True + + def __repr__(self) -> str: + type_str = "folder" if self.is_dir else "file" + return f"ObjectRef({type_str}: {self.path}, size={self.size})" + + def __str__(self) -> str: + return self.path diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 308b0452d..6fbbbff98 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -188,6 +188,34 @@ class ExternalSettings(BaseSettings): aws_secret_access_key: SecretStr | None = Field(default=None, validation_alias="DJ_AWS_SECRET_ACCESS_KEY") +class ObjectStorageSettings(BaseSettings): + """Object storage configuration for the object type.""" + + model_config = SettingsConfigDict( + env_prefix="DJ_OBJECT_STORAGE_", + case_sensitive=False, + extra="forbid", + validate_assignment=True, + ) + + # Required settings + project_name: str | None = Field(default=None, description="Unique project identifier") + protocol: str | None = Field(default=None, description="Storage protocol: file, s3, gcs, azure") + location: str | None = Field(default=None, description="Base path or bucket prefix") + + # Cloud storage settings + bucket: str | None = Field(default=None, description="Bucket name (S3, GCS)") + container: str | None = Field(default=None, description="Container name (Azure)") + endpoint: str | None = Field(default=None, description="S3 endpoint URL") + access_key: str | None = Field(default=None, description="Access key") + secret_key: SecretStr | None = Field(default=None, description="Secret key") + secure: bool = Field(default=True, description="Use HTTPS") + + # Optional settings + partition_pattern: str | None = Field(default=None, description="Path pattern with {attribute} placeholders") + token_length: int = Field(default=8, ge=4, le=16, description="Random suffix length for filenames") + + class Config(BaseSettings): """ Main DataJoint configuration. @@ -219,6 +247,7 @@ class Config(BaseSettings): connection: ConnectionSettings = Field(default_factory=ConnectionSettings) display: DisplaySettings = Field(default_factory=DisplaySettings) external: ExternalSettings = Field(default_factory=ExternalSettings) + object_storage: ObjectStorageSettings = Field(default_factory=ObjectStorageSettings) # Top-level settings loglevel: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default="INFO", validation_alias="DJ_LOG_LEVEL") @@ -336,6 +365,72 @@ def get_store_spec(self, store: str) -> dict[str, Any]: return spec + def get_object_storage_spec(self) -> dict[str, Any]: + """ + Get validated object storage configuration. + + Returns: + Object storage configuration dict + + Raises: + DataJointError: If object storage is not configured or has invalid config + """ + os_settings = self.object_storage + + # Check if object storage is configured + if not os_settings.protocol: + raise DataJointError( + "Object storage is not configured. Set object_storage.protocol in datajoint.json " + "or DJ_OBJECT_STORAGE_PROTOCOL environment variable." + ) + + if not os_settings.project_name: + raise DataJointError( + "Object storage project_name is required. Set object_storage.project_name in datajoint.json " + "or DJ_OBJECT_STORAGE_PROJECT_NAME environment variable." + ) + + protocol = os_settings.protocol.lower() + supported_protocols = ("file", "s3", "gcs", "azure") + if protocol not in supported_protocols: + raise DataJointError( + f"Invalid object_storage.protocol: {protocol}. " + f'Supported protocols: {", ".join(supported_protocols)}' + ) + + # Build spec dict + spec = { + "project_name": os_settings.project_name, + "protocol": protocol, + "location": os_settings.location or "", + "partition_pattern": os_settings.partition_pattern, + "token_length": os_settings.token_length, + } + + # Add protocol-specific settings + if protocol == "s3": + if not os_settings.endpoint or not os_settings.bucket: + raise DataJointError("object_storage.endpoint and object_storage.bucket are required for S3") + if not os_settings.access_key or not os_settings.secret_key: + raise DataJointError("object_storage.access_key and object_storage.secret_key are required for S3") + spec.update({ + "endpoint": os_settings.endpoint, + "bucket": os_settings.bucket, + "access_key": os_settings.access_key, + "secret_key": os_settings.secret_key.get_secret_value() if os_settings.secret_key else None, + "secure": os_settings.secure, + }) + elif protocol == "gcs": + if not os_settings.bucket: + raise DataJointError("object_storage.bucket is required for GCS") + spec["bucket"] = os_settings.bucket + elif protocol == "azure": + if not os_settings.container: + raise DataJointError("object_storage.container is required for Azure") + spec["container"] = os_settings.container + + return spec + def load(self, filename: str | Path) -> None: """ Load settings from a JSON file. diff --git a/src/datajoint/staged_insert.py b/src/datajoint/staged_insert.py new file mode 100644 index 000000000..8ccbd3952 --- /dev/null +++ b/src/datajoint/staged_insert.py @@ -0,0 +1,316 @@ +""" +Staged insert context manager for direct object storage writes. + +This module provides the StagedInsert class which allows writing directly +to object storage before finalizing the database insert. +""" + +import json +import mimetypes +from contextlib import contextmanager +from datetime import datetime, timezone +from pathlib import Path +from typing import IO, Any + +import fsspec + +from .errors import DataJointError +from .settings import config +from .storage import StorageBackend, build_object_path, generate_token + + +class StagedInsert: + """ + Context manager for staged insert operations. + + Allows direct writes to object storage before finalizing the database insert. + Used for large objects like Zarr arrays where copying from local storage + is inefficient. + + Usage: + with table.staged_insert1 as staged: + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Create object storage directly + z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(1000, 1000)) + z[:] = data + + # Assign to record + staged.rec['raw_data'] = z + + # On successful exit: metadata computed, record inserted + # On exception: storage cleaned up, no record inserted + """ + + def __init__(self, table): + """ + Initialize a staged insert. + + Args: + table: The Table instance to insert into + """ + self._table = table + self._rec: dict[str, Any] = {} + self._staged_objects: dict[str, dict] = {} # field -> {path, ext, token} + self._backend: StorageBackend | None = None + + @property + def rec(self) -> dict[str, Any]: + """Record dict for setting attribute values.""" + return self._rec + + @property + def fs(self) -> fsspec.AbstractFileSystem: + """Return fsspec filesystem for advanced operations.""" + self._ensure_backend() + return self._backend.fs + + def _ensure_backend(self): + """Ensure storage backend is initialized.""" + if self._backend is None: + try: + spec = config.get_object_storage_spec() + self._backend = StorageBackend(spec) + except DataJointError: + raise DataJointError( + "Object storage is not configured. Set object_storage settings in datajoint.json " + "or DJ_OBJECT_STORAGE_* environment variables." + ) + + def _get_storage_path(self, field: str, ext: str = "") -> str: + """ + Get or create the storage path for a field. + + Args: + field: Name of the object attribute + ext: Optional extension (e.g., ".zarr") + + Returns: + Full storage path + """ + self._ensure_backend() + + if field in self._staged_objects: + return self._staged_objects[field]["full_path"] + + # Validate field is an object attribute + if field not in self._table.heading: + raise DataJointError(f"Attribute '{field}' not found in table heading") + + attr = self._table.heading[field] + if not attr.is_object: + raise DataJointError(f"Attribute '{field}' is not an object type") + + # Extract primary key from rec + primary_key = {k: self._rec[k] for k in self._table.primary_key if k in self._rec} + if len(primary_key) != len(self._table.primary_key): + raise DataJointError( + "Primary key values must be set in staged.rec before calling store() or open(). " + f"Missing: {set(self._table.primary_key) - set(primary_key)}" + ) + + # Get storage spec + spec = config.get_object_storage_spec() + partition_pattern = spec.get("partition_pattern") + token_length = spec.get("token_length", 8) + location = spec.get("location", "") + + # Build storage path + relative_path, token = build_object_path( + schema=self._table.database, + table=self._table.class_name, + field=field, + primary_key=primary_key, + ext=ext if ext else None, + partition_pattern=partition_pattern, + token_length=token_length, + ) + + # Full path with location prefix + full_path = f"{location}/{relative_path}" if location else relative_path + + # Store staged object info + self._staged_objects[field] = { + "relative_path": relative_path, + "full_path": full_path, + "ext": ext if ext else None, + "token": token, + } + + return full_path + + def store(self, field: str, ext: str = "") -> fsspec.FSMap: + """ + Get an FSMap store for direct writes to an object field. + + Args: + field: Name of the object attribute + ext: Optional extension (e.g., ".zarr", ".hdf5") + + Returns: + fsspec.FSMap suitable for Zarr/xarray + """ + path = self._get_storage_path(field, ext) + return self._backend.get_fsmap(path) + + def open(self, field: str, ext: str = "", mode: str = "wb") -> IO: + """ + Open a file for direct writes to an object field. + + Args: + field: Name of the object attribute + ext: Optional extension (e.g., ".bin", ".dat") + mode: File mode (default: "wb") + + Returns: + File-like object for writing + """ + path = self._get_storage_path(field, ext) + return self._backend.open(path, mode) + + def _compute_metadata(self, field: str) -> dict: + """ + Compute metadata for a staged object after writing is complete. + + Args: + field: Name of the object attribute + + Returns: + JSON-serializable metadata dict + """ + info = self._staged_objects[field] + full_path = info["full_path"] + ext = info["ext"] + + # Check if it's a directory (multiple files) or single file + full_remote_path = self._backend._full_path(full_path) + + try: + is_dir = self._backend.fs.isdir(full_remote_path) + except Exception: + is_dir = False + + if is_dir: + # Calculate total size and file count + total_size = 0 + item_count = 0 + files = [] + + for root, dirs, filenames in self._backend.fs.walk(full_remote_path): + for filename in filenames: + file_path = f"{root}/{filename}" + try: + file_size = self._backend.fs.size(file_path) + rel_path = file_path[len(full_remote_path) :].lstrip("/") + files.append({"path": rel_path, "size": file_size}) + total_size += file_size + item_count += 1 + except Exception: + pass + + # Create manifest + manifest = { + "files": files, + "total_size": total_size, + "item_count": item_count, + "created": datetime.now(timezone.utc).isoformat(), + } + + # Write manifest alongside folder + manifest_path = f"{full_path}.manifest.json" + self._backend.put_buffer(json.dumps(manifest, indent=2).encode(), manifest_path) + + metadata = { + "path": info["relative_path"], + "size": total_size, + "hash": None, + "ext": ext, + "is_dir": True, + "timestamp": datetime.now(timezone.utc).isoformat(), + "item_count": item_count, + } + else: + # Single file + try: + size = self._backend.size(full_path) + except Exception: + size = 0 + + metadata = { + "path": info["relative_path"], + "size": size, + "hash": None, + "ext": ext, + "is_dir": False, + "timestamp": datetime.now(timezone.utc).isoformat(), + } + + # Add mime_type for files + if ext: + mime_type, _ = mimetypes.guess_type(f"file{ext}") + if mime_type: + metadata["mime_type"] = mime_type + + return metadata + + def _finalize(self): + """ + Finalize the staged insert by computing metadata and inserting the record. + """ + # Process each staged object + for field in list(self._staged_objects.keys()): + metadata = self._compute_metadata(field) + # Store JSON metadata in the record + self._rec[field] = json.dumps(metadata) + + # Insert the record + self._table.insert1(self._rec) + + def _cleanup(self): + """ + Clean up staged objects on failure. + """ + if self._backend is None: + return + + for field, info in self._staged_objects.items(): + full_path = info["full_path"] + try: + # Check if it's a directory + full_remote_path = self._backend._full_path(full_path) + if self._backend.fs.exists(full_remote_path): + if self._backend.fs.isdir(full_remote_path): + self._backend.remove_folder(full_path) + else: + self._backend.remove(full_path) + except Exception: + pass # Best effort cleanup + + +@contextmanager +def staged_insert1(table): + """ + Context manager for staged insert operations. + + Args: + table: The Table instance to insert into + + Yields: + StagedInsert instance for setting record values and getting storage handles + + Example: + with staged_insert1(Recording) as staged: + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + z = zarr.open(staged.store('raw_data', '.zarr'), mode='w') + z[:] = data + staged.rec['raw_data'] = z + """ + staged = StagedInsert(table) + try: + yield staged + staged._finalize() + except Exception: + staged._cleanup() + raise diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index 903bdc0d6..7d7e0ca35 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -5,7 +5,12 @@ backends (local filesystem, S3, GCS, Azure, etc.) using the fsspec library. """ +import json import logging +import mimetypes +import secrets +import urllib.parse +from datetime import datetime, timezone from pathlib import Path, PurePosixPath from typing import Any @@ -15,6 +20,127 @@ logger = logging.getLogger(__name__.split(".")[0]) +# Characters safe for use in filenames and URLs +TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + + +def generate_token(length: int = 8) -> str: + """ + Generate a random token for filename collision avoidance. + + Args: + length: Token length (4-16 characters, default 8) + + Returns: + Random URL-safe string + """ + length = max(4, min(16, length)) + return "".join(secrets.choice(TOKEN_ALPHABET) for _ in range(length)) + + +def encode_pk_value(value: Any) -> str: + """ + Encode a primary key value for use in storage paths. + + Args: + value: Primary key value (int, str, date, etc.) + + Returns: + Path-safe string representation + """ + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, datetime): + # Use ISO format with safe separators + return value.strftime("%Y-%m-%dT%H-%M-%S") + if hasattr(value, "isoformat"): + # Handle date objects + return value.isoformat() + + # String handling + s = str(value) + # Check if path-safe (no special characters) + unsafe_chars = "/\\:*?\"<>|" + if any(c in s for c in unsafe_chars) or len(s) > 100: + # URL-encode unsafe strings or truncate long ones + if len(s) > 100: + # Truncate and add hash suffix for uniqueness + import hashlib + + hash_suffix = hashlib.md5(s.encode()).hexdigest()[:8] + s = s[:50] + "_" + hash_suffix + return urllib.parse.quote(s, safe="") + return s + + +def build_object_path( + schema: str, + table: str, + field: str, + primary_key: dict[str, Any], + ext: str | None, + partition_pattern: str | None = None, + token_length: int = 8, +) -> tuple[str, str]: + """ + Build the storage path for an object attribute. + + Args: + schema: Schema name + table: Table name + field: Field/attribute name + primary_key: Dict of primary key attribute names to values + ext: File extension (e.g., ".dat") or None + partition_pattern: Optional partition pattern with {attr} placeholders + token_length: Length of random token suffix + + Returns: + Tuple of (relative_path, token) + """ + token = generate_token(token_length) + + # Build filename: field_token.ext + filename = f"{field}_{token}" + if ext: + if not ext.startswith("."): + ext = "." + ext + filename += ext + + # Build primary key path components + pk_parts = [] + partition_attrs = set() + + # Extract partition attributes if pattern specified + if partition_pattern: + import re + + partition_attrs = set(re.findall(r"\{(\w+)\}", partition_pattern)) + + # Build partition prefix (attributes specified in partition pattern) + partition_parts = [] + for attr in partition_attrs: + if attr in primary_key: + partition_parts.append(f"{attr}={encode_pk_value(primary_key[attr])}") + + # Build remaining PK path (attributes not in partition) + for attr, value in primary_key.items(): + if attr not in partition_attrs: + pk_parts.append(f"{attr}={encode_pk_value(value)}") + + # Construct full path + # Pattern: {partition_attrs}/{schema}/{table}/objects/{remaining_pk}/{filename} + parts = [] + if partition_parts: + parts.extend(partition_parts) + parts.append(schema) + parts.append(table) + parts.append("objects") + if pk_parts: + parts.extend(pk_parts) + parts.append(filename) + + return "/".join(parts), token + class StorageBackend: """ @@ -274,6 +400,104 @@ def open(self, remote_path: str | PurePosixPath, mode: str = "rb"): full_path = self._full_path(remote_path) return self.fs.open(full_path, mode) + def put_folder(self, local_path: str | Path, remote_path: str | PurePosixPath) -> dict: + """ + Upload a folder to storage. + + Args: + local_path: Path to local folder + remote_path: Destination path in storage + + Returns: + Manifest dict with file list, total_size, and item_count + """ + local_path = Path(local_path) + if not local_path.is_dir(): + raise errors.DataJointError(f"Not a directory: {local_path}") + + full_path = self._full_path(remote_path) + logger.debug(f"put_folder: {local_path} -> {self.protocol}:{full_path}") + + # Collect file info for manifest + files = [] + total_size = 0 + + for root, dirs, filenames in local_path.walk(): + for filename in filenames: + file_path = root / filename + rel_path = file_path.relative_to(local_path).as_posix() + file_size = file_path.stat().st_size + files.append({"path": rel_path, "size": file_size}) + total_size += file_size + + # Upload folder contents + if self.protocol == "file": + import shutil + + dest = Path(full_path) + dest.mkdir(parents=True, exist_ok=True) + for item in local_path.iterdir(): + if item.is_file(): + shutil.copy2(item, dest / item.name) + else: + shutil.copytree(item, dest / item.name, dirs_exist_ok=True) + else: + self.fs.put(str(local_path), full_path, recursive=True) + + # Build manifest + manifest = { + "files": files, + "total_size": total_size, + "item_count": len(files), + "created": datetime.now(timezone.utc).isoformat(), + } + + # Write manifest alongside folder + manifest_path = f"{remote_path}.manifest.json" + self.put_buffer(json.dumps(manifest, indent=2).encode(), manifest_path) + + return manifest + + def remove_folder(self, remote_path: str | PurePosixPath): + """ + Remove a folder and its manifest from storage. + + Args: + remote_path: Path to folder in storage + """ + full_path = self._full_path(remote_path) + logger.debug(f"remove_folder: {self.protocol}:{full_path}") + + try: + if self.protocol == "file": + import shutil + + shutil.rmtree(full_path, ignore_errors=True) + else: + self.fs.rm(full_path, recursive=True) + except FileNotFoundError: + pass + + # Also remove manifest + manifest_path = f"{remote_path}.manifest.json" + self.remove(manifest_path) + + def get_fsmap(self, remote_path: str | PurePosixPath) -> fsspec.FSMap: + """ + Get an FSMap for a path (useful for Zarr/xarray). + + Args: + remote_path: Path in storage + + Returns: + fsspec.FSMap instance + """ + full_path = self._full_path(remote_path) + return fsspec.FSMap(full_path, self.fs) + + +STORE_METADATA_FILENAME = "datajoint_store.json" + def get_storage_backend(spec: dict[str, Any]) -> StorageBackend: """ @@ -286,3 +510,69 @@ def get_storage_backend(spec: dict[str, Any]) -> StorageBackend: StorageBackend instance """ return StorageBackend(spec) + + +def verify_or_create_store_metadata(backend: StorageBackend, spec: dict[str, Any]) -> dict: + """ + Verify or create the store metadata file at the storage root. + + On first use, creates the datajoint_store.json file with project info. + On subsequent uses, verifies the project_name matches. + + Args: + backend: StorageBackend instance + spec: Object storage configuration spec + + Returns: + Store metadata dict + + Raises: + DataJointError: If project_name mismatch detected + """ + from .version import __version__ as dj_version + + project_name = spec.get("project_name") + location = spec.get("location", "") + + # Metadata file path at storage root + metadata_path = f"{location}/{STORE_METADATA_FILENAME}" if location else STORE_METADATA_FILENAME + + try: + # Try to read existing metadata + if backend.exists(metadata_path): + metadata_content = backend.get_buffer(metadata_path) + metadata = json.loads(metadata_content) + + # Verify project_name matches + store_project = metadata.get("project_name") + if store_project and store_project != project_name: + raise errors.DataJointError( + f"Object store project name mismatch.\n" + f' Client configured: "{project_name}"\n' + f' Store metadata: "{store_project}"\n' + f"Ensure all clients use the same object_storage.project_name setting." + ) + + return metadata + else: + # Create new metadata + metadata = { + "project_name": project_name, + "created": datetime.now(timezone.utc).isoformat(), + "format_version": "1.0", + "datajoint_version": dj_version, + } + + # Optional database info - not enforced, just informational + # These would need to be passed in from the connection context + # For now, omit them + + backend.put_buffer(json.dumps(metadata, indent=2).encode(), metadata_path) + return metadata + + except errors.DataJointError: + raise + except Exception as e: + # Log warning but don't fail - metadata is informational + logger.warning(f"Could not verify/create store metadata: {e}") + return {"project_name": project_name} diff --git a/src/datajoint/table.py b/src/datajoint/table.py index a8a52c3e0..2d7ffb852 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -4,9 +4,11 @@ import itertools import json import logging +import mimetypes import platform import re import uuid +from datetime import datetime, timezone from pathlib import Path import numpy as np @@ -25,6 +27,8 @@ from .expression import QueryExpression from .heading import Heading from .settings import config +from .staged_insert import staged_insert1 as _staged_insert1 +from .storage import StorageBackend, build_object_path, verify_or_create_store_metadata from .utils import get_master, is_camel_case, user_choice from .version import __version__ as version @@ -269,6 +273,128 @@ def _log(self): def external(self): return self.connection.schemas[self.database].external + @property + def object_storage(self) -> StorageBackend | None: + """Get the object storage backend for this table.""" + if not hasattr(self, "_object_storage"): + try: + spec = config.get_object_storage_spec() + self._object_storage = StorageBackend(spec) + # Verify/create store metadata on first use + verify_or_create_store_metadata(self._object_storage, spec) + except DataJointError: + self._object_storage = None + return self._object_storage + + def _process_object_value(self, name: str, value, row: dict) -> str: + """ + Process an object attribute value for insert. + + Args: + name: Attribute name + value: Input value (file path, folder path, or (ext, stream) tuple) + row: The full row dict (needed for primary key values) + + Returns: + JSON string for database storage + """ + if self.object_storage is None: + raise DataJointError( + "Object storage is not configured. Set object_storage settings in datajoint.json " + "or DJ_OBJECT_STORAGE_* environment variables." + ) + + # Extract primary key values from row + primary_key = {k: row[k] for k in self.primary_key if k in row} + if not primary_key: + raise DataJointError( + "Primary key values must be provided before object attributes for insert." + ) + + # Determine input type and extract extension + is_dir = False + ext = None + size = 0 + source_path = None + stream = None + + if isinstance(value, tuple) and len(value) == 2: + # Tuple of (ext, stream) + ext, stream = value + if hasattr(stream, "read"): + # Read stream to buffer for upload + content = stream.read() + size = len(content) + else: + raise DataJointError(f"Invalid stream object for attribute {name}") + elif isinstance(value, (str, Path)): + source_path = Path(value) + if not source_path.exists(): + raise DataJointError(f"File or folder not found: {source_path}") + is_dir = source_path.is_dir() + if not is_dir: + ext = source_path.suffix or None + size = source_path.stat().st_size + else: + raise DataJointError( + f"Invalid value type for object attribute {name}. " + "Expected file path, folder path, or (ext, stream) tuple." + ) + + # Get storage spec for path building + spec = config.get_object_storage_spec() + partition_pattern = spec.get("partition_pattern") + token_length = spec.get("token_length", 8) + location = spec.get("location", "") + + # Build storage path + relative_path, token = build_object_path( + schema=self.database, + table=self.class_name, + field=name, + primary_key=primary_key, + ext=ext, + partition_pattern=partition_pattern, + token_length=token_length, + ) + + # Prepend location if specified + full_storage_path = f"{location}/{relative_path}" if location else relative_path + + # Upload content + manifest = None + if source_path: + if is_dir: + manifest = self.object_storage.put_folder(source_path, full_storage_path) + size = manifest["total_size"] + else: + self.object_storage.put_file(source_path, full_storage_path) + elif stream: + self.object_storage.put_buffer(content, full_storage_path) + + # Build JSON metadata + timestamp = datetime.now(timezone.utc).isoformat() + metadata = { + "path": relative_path, + "size": size, + "hash": None, # Hash is optional, not computed by default + "ext": ext, + "is_dir": is_dir, + "timestamp": timestamp, + } + + # Add mime_type for files + if not is_dir and ext: + mime_type, _ = mimetypes.guess_type(f"file{ext}") + if mime_type: + metadata["mime_type"] = mime_type + + # Add item_count for folders + if is_dir and manifest: + metadata["item_count"] = manifest["item_count"] + + return json.dumps(metadata) + def update1(self, row): """ ``update1`` updates one existing entry in the table. @@ -320,6 +446,35 @@ def insert1(self, row, **kwargs): """ self.insert((row,), **kwargs) + @property + def staged_insert1(self): + """ + Context manager for staged insert with direct object storage writes. + + Use this for large objects like Zarr arrays where copying from local storage + is inefficient. Allows writing directly to the destination storage before + finalizing the database insert. + + Example: + with table.staged_insert1 as staged: + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Create object storage directly + z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(1000, 1000)) + z[:] = data + + # Assign to record + staged.rec['raw_data'] = z + + # On successful exit: metadata computed, record inserted + # On exception: storage cleaned up, no record inserted + + Yields: + StagedInsert: Context for setting record values and getting storage handles + """ + return _staged_insert1(self) + def insert( self, rows, @@ -713,7 +868,7 @@ def describe(self, context=None, printout=False): return definition # --- private helper functions ---- - def __make_placeholder(self, name, value, ignore_extra_fields=False): + def __make_placeholder(self, name, value, ignore_extra_fields=False, row=None): """ For a given attribute `name` with `value`, return its processed value or value placeholder as a string to be included in the query and the value, if any, to be submitted for @@ -721,6 +876,8 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False): :param name: name of attribute to be inserted :param value: value of attribute to be inserted + :param ignore_extra_fields: if True, return None for unknown fields + :param row: the full row dict (needed for object attributes to extract primary key) """ if ignore_extra_fields and name not in self.heading: return None @@ -752,6 +909,14 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False): value = str.encode(attachment_path.name) + b"\0" + attachment_path.read_bytes() elif attr.is_filepath: value = self.external[attr.store].upload_filepath(value).bytes + elif attr.is_object: + # Object type - upload to object storage and return JSON metadata + if row is None: + raise DataJointError( + f"Object attribute {name} requires full row context for insert. " + "This is an internal error." + ) + value = self._process_object_value(name, value, row) elif attr.numeric: value = str(int(value) if isinstance(value, bool) else value) elif attr.json: @@ -780,17 +945,21 @@ def check_fields(fields): elif set(field_list) != set(fields).intersection(self.heading.names): raise DataJointError("Attempt to insert rows with different fields.") + # Convert row to dict for object attribute processing + row_dict = None if isinstance(row, np.void): # np.array check_fields(row.dtype.fields) + row_dict = {name: row[name] for name in row.dtype.fields} attributes = [ - self.__make_placeholder(name, row[name], ignore_extra_fields) + self.__make_placeholder(name, row[name], ignore_extra_fields, row=row_dict) for name in self.heading if name in row.dtype.fields ] elif isinstance(row, collections.abc.Mapping): # dict-based check_fields(row) + row_dict = dict(row) attributes = [ - self.__make_placeholder(name, row[name], ignore_extra_fields) for name in self.heading if name in row + self.__make_placeholder(name, row[name], ignore_extra_fields, row=row_dict) for name in self.heading if name in row ] else: # positional try: @@ -803,8 +972,9 @@ def check_fields(fields): except TypeError: raise DataJointError("Datatype %s cannot be inserted" % type(row)) else: + row_dict = dict(zip(self.heading.names, row)) attributes = [ - self.__make_placeholder(name, value, ignore_extra_fields) for name, value in zip(self.heading, row) + self.__make_placeholder(name, value, ignore_extra_fields, row=row_dict) for name, value in zip(self.heading, row) ] if ignore_extra_fields: attributes = [a for a in attributes if a is not None] From b45df2c1cd9905e94aacb4cfbe036875e769d31d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:11:31 +0000 Subject: [PATCH 068/219] Fix ruff lint: line length and unused imports --- src/datajoint/heading.py | 10 ++++++++-- src/datajoint/table.py | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index 1cc66afde..37c280c5a 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -137,7 +137,10 @@ def blobs(self): @property def non_blobs(self): - return [k for k, v in self.attributes.items() if not (v.is_blob or v.is_attachment or v.is_filepath or v.is_object or v.json)] + return [ + k for k, v in self.attributes.items() + if not (v.is_blob or v.is_attachment or v.is_filepath or v.is_object or v.json) + ] @property def new_attributes(self): @@ -344,7 +347,10 @@ def _init_from_database(self): attr["json"], ) ): - raise DataJointError("Json, Blob, attachment, filepath, or object attributes are not allowed in the primary key") + raise DataJointError( + "Json, Blob, attachment, filepath, or object attributes " + "are not allowed in the primary key" + ) if attr["string"] and attr["default"] is not None and attr["default"] not in sql_literals: attr["default"] = '"%s"' % attr["default"] diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 2d7ffb852..967f640fe 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -959,7 +959,8 @@ def check_fields(fields): check_fields(row) row_dict = dict(row) attributes = [ - self.__make_placeholder(name, row[name], ignore_extra_fields, row=row_dict) for name in self.heading if name in row + self.__make_placeholder(name, row[name], ignore_extra_fields, row=row_dict) + for name in self.heading if name in row ] else: # positional try: @@ -974,7 +975,8 @@ def check_fields(fields): else: row_dict = dict(zip(self.heading.names, row)) attributes = [ - self.__make_placeholder(name, value, ignore_extra_fields, row=row_dict) for name, value in zip(self.heading, row) + self.__make_placeholder(name, value, ignore_extra_fields, row=row_dict) + for name, value in zip(self.heading, row) ] if ignore_extra_fields: attributes = [a for a in attributes if a is not None] From adf4305b90fc830283ebbdf44780bdfeb42d5d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:16:40 +0000 Subject: [PATCH 069/219] Fix unused imports (ruff lint) Remove unused mimetypes imports from objectref.py and storage.py, remove unused Path import and generate_token from staged_insert.py, and fix f-string without placeholders in objectref.py. --- src/datajoint/objectref.py | 5 ++--- src/datajoint/staged_insert.py | 3 +-- src/datajoint/storage.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/datajoint/objectref.py b/src/datajoint/objectref.py index 8707e060f..f3cfffef8 100644 --- a/src/datajoint/objectref.py +++ b/src/datajoint/objectref.py @@ -7,11 +7,10 @@ """ import json -import mimetypes from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import IO, Any, Iterator +from typing import IO, Iterator import fsspec @@ -345,7 +344,7 @@ def _verify_folder(self) -> bool: errors.append(f"Size mismatch for {file_info['path']}: expected {expected_size}, got {actual_size}") if errors: - raise IntegrityError(f"Folder verification failed:\n" + "\n".join(errors)) + raise IntegrityError("Folder verification failed:\n" + "\n".join(errors)) return True diff --git a/src/datajoint/staged_insert.py b/src/datajoint/staged_insert.py index 8ccbd3952..9083bb78b 100644 --- a/src/datajoint/staged_insert.py +++ b/src/datajoint/staged_insert.py @@ -9,14 +9,13 @@ import mimetypes from contextlib import contextmanager from datetime import datetime, timezone -from pathlib import Path from typing import IO, Any import fsspec from .errors import DataJointError from .settings import config -from .storage import StorageBackend, build_object_path, generate_token +from .storage import StorageBackend, build_object_path class StagedInsert: diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index 7d7e0ca35..719fe367f 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -7,7 +7,6 @@ import json import logging -import mimetypes import secrets import urllib.parse from datetime import datetime, timezone From 095753f31b35d7f9bf7da3b6d3c2a37225b49ba6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:21:09 +0000 Subject: [PATCH 070/219] Add documentation for object column type - Create comprehensive object.md page covering configuration, insert, fetch, staged inserts, and integration with Zarr/xarray - Update attributes.md to list object as a special DataJoint datatype - Add object_storage configuration section to settings.md - Add ObjectRef and array library integration section to fetch.md - Add object attributes and staged_insert1 section to insert.md --- docs/src/client/settings.md | 54 +++++ docs/src/design/tables/attributes.md | 3 + docs/src/design/tables/object.md | 326 +++++++++++++++++++++++++++ docs/src/manipulation/insert.md | 63 ++++++ docs/src/query/fetch.md | 48 ++++ 5 files changed, 494 insertions(+) create mode 100644 docs/src/design/tables/object.md diff --git a/docs/src/client/settings.md b/docs/src/client/settings.md index d9fd468a2..06bee4f87 100644 --- a/docs/src/client/settings.md +++ b/docs/src/client/settings.md @@ -164,3 +164,57 @@ Configure external stores in the `stores` section. See [External Storage](../sys } } ``` + +## Object Storage + +Configure object storage for the [`object` type](../design/tables/object.md) in the `object_storage` section. This provides managed file and folder storage with fsspec backend support. + +### Local Filesystem + +```json +{ + "object_storage": { + "project_name": "my_project", + "protocol": "file", + "location": "/data/my_project" + } +} +``` + +### Amazon S3 + +```json +{ + "object_storage": { + "project_name": "my_project", + "protocol": "s3", + "bucket": "my-bucket", + "location": "my_project", + "endpoint": "s3.amazonaws.com" + } +} +``` + +### Object Storage Settings + +| Setting | Environment Variable | Required | Description | +|---------|---------------------|----------|-------------| +| `object_storage.project_name` | `DJ_OBJECT_STORAGE_PROJECT_NAME` | Yes | Unique project identifier | +| `object_storage.protocol` | `DJ_OBJECT_STORAGE_PROTOCOL` | Yes | Backend: `file`, `s3`, `gcs`, `azure` | +| `object_storage.location` | `DJ_OBJECT_STORAGE_LOCATION` | Yes | Base path or bucket prefix | +| `object_storage.bucket` | `DJ_OBJECT_STORAGE_BUCKET` | For cloud | Bucket name | +| `object_storage.endpoint` | `DJ_OBJECT_STORAGE_ENDPOINT` | For S3 | S3 endpoint URL | +| `object_storage.partition_pattern` | `DJ_OBJECT_STORAGE_PARTITION_PATTERN` | No | Path pattern with `{attr}` placeholders | +| `object_storage.token_length` | `DJ_OBJECT_STORAGE_TOKEN_LENGTH` | No | Random suffix length (default: 8) | +| `object_storage.access_key` | — | For cloud | Access key (use secrets) | +| `object_storage.secret_key` | — | For cloud | Secret key (use secrets) | + +### Object Storage Secrets + +Store cloud credentials in the secrets directory: + +``` +.secrets/ +├── object_storage.access_key +└── object_storage.secret_key +``` diff --git a/docs/src/design/tables/attributes.md b/docs/src/design/tables/attributes.md index 9363e527f..1a5d6b308 100644 --- a/docs/src/design/tables/attributes.md +++ b/docs/src/design/tables/attributes.md @@ -71,6 +71,9 @@ info). These types abstract certain kinds of non-database data to facilitate use together with DataJoint. +- `object`: managed [file and folder storage](object.md) with support for direct writes +(Zarr, HDF5) and fsspec integration. Recommended for new pipelines. + - `attach`: a [file attachment](attach.md) similar to email attachments facillitating sending/receiving an opaque data file to/from a DataJoint pipeline. diff --git a/docs/src/design/tables/object.md b/docs/src/design/tables/object.md new file mode 100644 index 000000000..2efe0c0af --- /dev/null +++ b/docs/src/design/tables/object.md @@ -0,0 +1,326 @@ +# Object Type + +The `object` type provides managed file and folder storage for DataJoint pipelines. Unlike `attach@store` and `filepath@store` which reference named stores, the `object` type uses a unified storage backend configured at the pipeline level. + +## Overview + +The `object` type supports both files and folders: + +- **Files**: Copied to storage at insert time, accessed via handle on fetch +- **Folders**: Entire directory trees stored as a unit (e.g., Zarr arrays) +- **Staged inserts**: Write directly to storage for large objects + +### Key Features + +- **Unified storage**: One storage backend per pipeline (local filesystem or cloud) +- **No hidden tables**: Metadata stored inline as JSON (simpler than `attach@store`) +- **fsspec integration**: Direct access for Zarr, xarray, and other array libraries +- **Immutable objects**: Content cannot be modified after insert + +## Configuration + +Configure object storage in `datajoint.json`: + +```json +{ + "object_storage": { + "project_name": "my_project", + "protocol": "s3", + "bucket": "my-bucket", + "location": "my_project", + "endpoint": "s3.amazonaws.com" + } +} +``` + +For local filesystem storage: + +```json +{ + "object_storage": { + "project_name": "my_project", + "protocol": "file", + "location": "/data/my_project" + } +} +``` + +### Configuration Options + +| Setting | Required | Description | +|---------|----------|-------------| +| `project_name` | Yes | Unique project identifier | +| `protocol` | Yes | Storage backend: `file`, `s3`, `gcs`, `azure` | +| `location` | Yes | Base path or bucket prefix | +| `bucket` | For cloud | Bucket name (S3, GCS, Azure) | +| `endpoint` | For S3 | S3 endpoint URL | +| `partition_pattern` | No | Path pattern with `{attribute}` placeholders | +| `token_length` | No | Random suffix length (default: 8, range: 4-16) | + +### Environment Variables + +Settings can be overridden via environment variables: + +```bash +DJ_OBJECT_STORAGE_PROTOCOL=s3 +DJ_OBJECT_STORAGE_BUCKET=my-bucket +DJ_OBJECT_STORAGE_LOCATION=my_project +``` + +## Table Definition + +Define an object attribute in your table: + +```python +@schema +class Recording(dj.Manual): + definition = """ + subject_id : int + session_id : int + --- + raw_data : object # managed file storage + processed : object # another object attribute + """ +``` + +Note: No `@store` suffix needed—storage is determined by pipeline configuration. + +## Insert Operations + +### Inserting Files + +Insert a file by providing its path: + +```python +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "/local/path/to/recording.dat" +}) +``` + +The file is copied to object storage and the path is stored as JSON metadata. + +### Inserting Folders + +Insert an entire directory: + +```python +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "/local/path/to/data_folder/" +}) +``` + +### Inserting from Streams + +Insert from a file-like object with explicit extension: + +```python +with open("/local/path/data.bin", "rb") as f: + Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": (".bin", f) + }) +``` + +### Staged Insert (Direct Write) + +For large objects like Zarr arrays, use staged insert to write directly to storage without a local copy: + +```python +import zarr + +with Recording.staged_insert1 as staged: + # Set primary key values first + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Create Zarr array directly in object storage + z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(10000, 10000)) + z[:] = compute_large_array() + + # Assign to record + staged.rec['raw_data'] = z + +# On successful exit: metadata computed, record inserted +# On exception: storage cleaned up, no record inserted +``` + +The `staged_insert1` context manager provides: + +- `staged.rec`: Dict for setting attribute values +- `staged.store(field, ext)`: Returns `fsspec.FSMap` for Zarr/xarray +- `staged.open(field, ext, mode)`: Returns file handle for writing +- `staged.fs`: Direct fsspec filesystem access + +## Fetch Operations + +Fetching an object attribute returns an `ObjectRef` handle: + +```python +record = Recording.fetch1() +obj = record["raw_data"] + +# Access metadata (no I/O) +print(obj.path) # Storage path +print(obj.size) # Size in bytes +print(obj.ext) # File extension (e.g., ".dat") +print(obj.is_dir) # True if folder +``` + +### Reading File Content + +```python +# Read entire file as bytes +content = obj.read() + +# Open as file object +with obj.open() as f: + data = f.read() +``` + +### Working with Folders + +```python +# List contents +contents = obj.listdir() + +# Walk directory tree +for root, dirs, files in obj.walk(): + print(root, files) + +# Open specific file in folder +with obj.open("subdir/file.dat") as f: + data = f.read() +``` + +### Downloading Files + +Download to local filesystem: + +```python +# Download entire object +local_path = obj.download("/local/destination/") + +# Download specific file from folder +local_path = obj.download("/local/destination/", "subdir/file.dat") +``` + +### Integration with Zarr and xarray + +The `ObjectRef` provides direct fsspec access: + +```python +import zarr +import xarray as xr + +record = Recording.fetch1() +obj = record["raw_data"] + +# Open as Zarr array +z = zarr.open(obj.store, mode='r') +print(z.shape) + +# Open with xarray +ds = xr.open_zarr(obj.store) + +# Access fsspec filesystem directly +fs = obj.fs +files = fs.ls(obj.full_path) +``` + +### Verifying Integrity + +Verify that stored content matches metadata: + +```python +try: + obj.verify() + print("Object integrity verified") +except IntegrityError as e: + print(f"Verification failed: {e}") +``` + +For files, this checks size (and hash if available). For folders, it validates the manifest. + +## Storage Structure + +Objects are stored with a deterministic path structure: + +``` +{location}/{schema}/{Table}/objects/{pk_attrs}/{field}_{token}{ext} +``` + +Example: +``` +my_project/my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat +``` + +### Partitioning + +Use `partition_pattern` to organize files by attributes: + +```json +{ + "object_storage": { + "partition_pattern": "{subject_id}/{session_id}" + } +} +``` + +This promotes specified attributes to the path root for better organization: + +``` +my_project/subject_id=123/session_id=45/my_schema/Recording/objects/raw_data_Ax7bQ2kM.dat +``` + +## Database Storage + +The `object` type is stored as a JSON column containing metadata: + +```json +{ + "path": "my_schema/Recording/objects/subject_id=123/raw_data_Ax7bQ2kM.dat", + "size": 12345, + "hash": null, + "ext": ".dat", + "is_dir": false, + "timestamp": "2025-01-15T10:30:00Z", + "mime_type": "application/octet-stream" +} +``` + +For folders, the metadata includes `item_count` and a manifest file is stored alongside the folder in object storage. + +## Comparison with Other Types + +| Feature | `attach@store` | `filepath@store` | `object` | +|---------|----------------|------------------|----------| +| Store config | Per-attribute | Per-attribute | Per-pipeline | +| Path control | DataJoint | User-managed | DataJoint | +| Hidden tables | Yes | Yes | **No** | +| Backend | File/S3 only | File/S3 only | fsspec (any) | +| Metadata storage | External table | External table | Inline JSON | +| Folder support | No | No | **Yes** | +| Direct write | No | No | **Yes** | + +## Delete Behavior + +When a record is deleted: + +1. Database record is deleted first (within transaction) +2. Storage file/folder deletion is attempted after commit +3. File deletion failures are logged but don't fail the transaction + +Orphaned files (from failed deletes or crashed inserts) can be cleaned up using maintenance utilities. + +## Best Practices + +1. **Use staged insert for large objects**: Avoid copying multi-gigabyte files through local storage +2. **Set primary keys before calling `store()`**: The storage path depends on primary key values +3. **Use meaningful extensions**: Extensions like `.zarr`, `.hdf5` help identify content type +4. **Verify after critical inserts**: Call `obj.verify()` for important data +5. **Configure partitioning for large datasets**: Improves storage organization and browsing diff --git a/docs/src/manipulation/insert.md b/docs/src/manipulation/insert.md index c64e55f17..753e73b6c 100644 --- a/docs/src/manipulation/insert.md +++ b/docs/src/manipulation/insert.md @@ -92,3 +92,66 @@ phase_two.Protocol.insert(phase_one.Protocol) protocols = phase_one.Protocol.fetch() phase_two.Protocol.insert(protocols) ``` + +## Object attributes + +Tables with [`object`](../design/tables/object.md) type attributes can be inserted with +file paths, folder paths, or streams. The content is automatically copied to object +storage. + +```python +# Insert with file path +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "/local/path/to/data.dat" +}) + +# Insert with folder path +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "/local/path/to/data_folder/" +}) + +# Insert from stream with explicit extension +with open("/path/to/data.bin", "rb") as f: + Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": (".bin", f) + }) +``` + +### Staged inserts + +For large objects like Zarr arrays, use `staged_insert1` to write directly to storage +without creating a local copy first: + +```python +import zarr + +with Recording.staged_insert1 as staged: + # Set primary key values first + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Create Zarr array directly in object storage + z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(10000, 10000)) + z[:] = compute_large_array() + + # Assign to record + staged.rec['raw_data'] = z + +# On successful exit: metadata computed, record inserted +# On exception: storage cleaned up, no record inserted +``` + +The `staged_insert1` context manager provides: + +- `staged.rec`: Dict for setting attribute values +- `staged.store(field, ext)`: Returns fsspec store for Zarr/xarray +- `staged.open(field, ext, mode)`: Returns file handle for writing +- `staged.fs`: Direct fsspec filesystem access + +See the [object type documentation](../design/tables/object.md) for more details. diff --git a/docs/src/query/fetch.md b/docs/src/query/fetch.md index 105d70084..75a50fd0d 100644 --- a/docs/src/query/fetch.md +++ b/docs/src/query/fetch.md @@ -124,3 +124,51 @@ frame = tab.fetch(format="frame") Returning results as a `DataFrame` is not possible when fetching a particular subset of attributes or when `as_dict` is set to `True`. + +## Object Attributes + +When fetching [`object`](../design/tables/object.md) attributes, DataJoint returns an +`ObjectRef` handle instead of the raw data. This allows working with large files without +copying them locally. + +```python +record = Recording.fetch1() +obj = record["raw_data"] + +# Access metadata (no I/O) +print(obj.path) # Storage path +print(obj.size) # Size in bytes +print(obj.is_dir) # True if folder + +# Read content +content = obj.read() # Returns bytes for files + +# Open as file object +with obj.open() as f: + data = f.read() + +# Download to local path +local_path = obj.download("/local/destination/") +``` + +### Integration with Array Libraries + +`ObjectRef` provides direct fsspec access for Zarr and xarray: + +```python +import zarr +import xarray as xr + +obj = Recording.fetch1()["neural_data"] + +# Open as Zarr array +z = zarr.open(obj.store, mode='r') + +# Open with xarray +ds = xr.open_zarr(obj.store) + +# Direct filesystem access +fs = obj.fs +``` + +See the [object type documentation](../design/tables/object.md) for more details. From 08838f63882f0922e0476f3c7084242b8b51f9f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:23:57 +0000 Subject: [PATCH 071/219] Fix ruff-format: code formatting adjustments Apply ruff formatter changes for consistent code style. --- src/datajoint/heading.py | 6 +++--- src/datajoint/settings.py | 19 ++++++++++--------- src/datajoint/storage.py | 2 +- src/datajoint/table.py | 13 +++++-------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index 37c280c5a..58f46cc0d 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -138,7 +138,8 @@ def blobs(self): @property def non_blobs(self): return [ - k for k, v in self.attributes.items() + k + for k, v in self.attributes.items() if not (v.is_blob or v.is_attachment or v.is_filepath or v.is_object or v.json) ] @@ -348,8 +349,7 @@ def _init_from_database(self): ) ): raise DataJointError( - "Json, Blob, attachment, filepath, or object attributes " - "are not allowed in the primary key" + "Json, Blob, attachment, filepath, or object attributes " "are not allowed in the primary key" ) if attr["string"] and attr["default"] is not None and attr["default"] not in sql_literals: diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 6fbbbff98..8e682691c 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -394,8 +394,7 @@ def get_object_storage_spec(self) -> dict[str, Any]: supported_protocols = ("file", "s3", "gcs", "azure") if protocol not in supported_protocols: raise DataJointError( - f"Invalid object_storage.protocol: {protocol}. " - f'Supported protocols: {", ".join(supported_protocols)}' + f"Invalid object_storage.protocol: {protocol}. " f'Supported protocols: {", ".join(supported_protocols)}' ) # Build spec dict @@ -413,13 +412,15 @@ def get_object_storage_spec(self) -> dict[str, Any]: raise DataJointError("object_storage.endpoint and object_storage.bucket are required for S3") if not os_settings.access_key or not os_settings.secret_key: raise DataJointError("object_storage.access_key and object_storage.secret_key are required for S3") - spec.update({ - "endpoint": os_settings.endpoint, - "bucket": os_settings.bucket, - "access_key": os_settings.access_key, - "secret_key": os_settings.secret_key.get_secret_value() if os_settings.secret_key else None, - "secure": os_settings.secure, - }) + spec.update( + { + "endpoint": os_settings.endpoint, + "bucket": os_settings.bucket, + "access_key": os_settings.access_key, + "secret_key": os_settings.secret_key.get_secret_value() if os_settings.secret_key else None, + "secure": os_settings.secure, + } + ) elif protocol == "gcs": if not os_settings.bucket: raise DataJointError("object_storage.bucket is required for GCS") diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index 719fe367f..c8b5c7b68 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -59,7 +59,7 @@ def encode_pk_value(value: Any) -> str: # String handling s = str(value) # Check if path-safe (no special characters) - unsafe_chars = "/\\:*?\"<>|" + unsafe_chars = '/\\:*?"<>|' if any(c in s for c in unsafe_chars) or len(s) > 100: # URL-encode unsafe strings or truncate long ones if len(s) > 100: diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 967f640fe..82dea15d3 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -307,9 +307,7 @@ def _process_object_value(self, name: str, value, row: dict) -> str: # Extract primary key values from row primary_key = {k: row[k] for k in self.primary_key if k in row} if not primary_key: - raise DataJointError( - "Primary key values must be provided before object attributes for insert." - ) + raise DataJointError("Primary key values must be provided before object attributes for insert.") # Determine input type and extract extension is_dir = False @@ -337,8 +335,7 @@ def _process_object_value(self, name: str, value, row: dict) -> str: size = source_path.stat().st_size else: raise DataJointError( - f"Invalid value type for object attribute {name}. " - "Expected file path, folder path, or (ext, stream) tuple." + f"Invalid value type for object attribute {name}. " "Expected file path, folder path, or (ext, stream) tuple." ) # Get storage spec for path building @@ -913,8 +910,7 @@ def __make_placeholder(self, name, value, ignore_extra_fields=False, row=None): # Object type - upload to object storage and return JSON metadata if row is None: raise DataJointError( - f"Object attribute {name} requires full row context for insert. " - "This is an internal error." + f"Object attribute {name} requires full row context for insert. " "This is an internal error." ) value = self._process_object_value(name, value, row) elif attr.numeric: @@ -960,7 +956,8 @@ def check_fields(fields): row_dict = dict(row) attributes = [ self.__make_placeholder(name, row[name], ignore_extra_fields, row=row_dict) - for name in self.heading if name in row + for name in self.heading + if name in row ] else: # positional try: From 3da69fd27c34268a0b29858bad5a87b6649470ed Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:27:18 +0000 Subject: [PATCH 072/219] Add pytest tests for object column type - schema_object.py: Test table definitions for object type - test_object.py: Comprehensive tests covering: - Storage path generation utilities - Insert with file, folder, and stream - Fetch returning ObjectRef - ObjectRef methods (read, open, download, listdir, walk, verify) - Staged insert operations - Error cases - conftest.py: Object storage fixtures for testing --- tests/conftest.py | 65 ++++ tests/schema_object.py | 51 +++ tests/test_object.py | 737 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 853 insertions(+) create mode 100644 tests/schema_object.py create mode 100644 tests/test_object.py diff --git a/tests/conftest.py b/tests/conftest.py index 8a6ba4057..136543fa8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -903,3 +903,68 @@ def channel(schema_any): @pytest.fixture def trash(schema_any): return schema.UberTrash() + + +# Object storage fixtures +from . import schema_object + + +@pytest.fixture +def object_storage_config(tmpdir_factory): + """Create object storage configuration for testing.""" + location = str(tmpdir_factory.mktemp("object_storage")) + return { + "project_name": "test_project", + "protocol": "file", + "location": location, + "token_length": 8, + } + + +@pytest.fixture +def mock_object_storage(object_storage_config, monkeypatch): + """Mock object storage configuration in datajoint config.""" + # Store original config + original_object_storage = getattr(dj.config, "_object_storage", None) + + # Create a mock ObjectStorageSettings-like object + class MockObjectStorageSettings: + def __init__(self, config): + self.project_name = config["project_name"] + self.protocol = config["protocol"] + self.location = config["location"] + self.token_length = config.get("token_length", 8) + self.partition_pattern = config.get("partition_pattern") + self.bucket = config.get("bucket") + self.endpoint = config.get("endpoint") + self.access_key = config.get("access_key") + self.secret_key = config.get("secret_key") + self.secure = config.get("secure", True) + self.container = config.get("container") + + mock_settings = MockObjectStorageSettings(object_storage_config) + + # Patch the object_storage attribute + monkeypatch.setattr(dj.config, "object_storage", mock_settings) + + yield object_storage_config + + # Restore original + if original_object_storage is not None: + monkeypatch.setattr(dj.config, "object_storage", original_object_storage) + + +@pytest.fixture +def schema_obj(connection_test, prefix, mock_object_storage): + """Schema for object type tests.""" + schema = dj.Schema( + prefix + "_object", + context=schema_object.LOCALS_OBJECT, + connection=connection_test, + ) + schema(schema_object.ObjectFile) + schema(schema_object.ObjectFolder) + schema(schema_object.ObjectMultiple) + schema(schema_object.ObjectWithOther) + yield schema + schema.drop() diff --git a/tests/schema_object.py b/tests/schema_object.py new file mode 100644 index 000000000..fe5215a37 --- /dev/null +++ b/tests/schema_object.py @@ -0,0 +1,51 @@ +""" +Schema definitions for object type tests. +""" + +import datajoint as dj + +LOCALS_OBJECT = locals() + + +class ObjectFile(dj.Manual): + """Table for testing object type with files.""" + + definition = """ + file_id : int + --- + data_file : object # stored file + """ + + +class ObjectFolder(dj.Manual): + """Table for testing object type with folders.""" + + definition = """ + folder_id : int + --- + data_folder : object # stored folder + """ + + +class ObjectMultiple(dj.Manual): + """Table for testing multiple object attributes.""" + + definition = """ + record_id : int + --- + raw_data : object # raw data file + processed : object # processed data file + """ + + +class ObjectWithOther(dj.Manual): + """Table for testing object type with other attributes.""" + + definition = """ + subject_id : int + session_id : int + --- + name : varchar(100) + data_file : object + notes : varchar(255) + """ diff --git a/tests/test_object.py b/tests/test_object.py new file mode 100644 index 000000000..decd1acae --- /dev/null +++ b/tests/test_object.py @@ -0,0 +1,737 @@ +""" +Tests for the object column type. + +Tests cover: +- Storage path generation +- Insert with file, folder, and stream +- Fetch returning ObjectRef +- ObjectRef methods (read, open, download, listdir, walk, verify) +- Staged insert +- Error cases +""" + +import io +import json +import os +from pathlib import Path + +import pytest + +import datajoint as dj +from datajoint.objectref import ObjectRef, IntegrityError +from datajoint.storage import build_object_path, generate_token, encode_pk_value + +from .schema_object import ObjectFile, ObjectFolder, ObjectMultiple, ObjectWithOther + + +class TestStoragePathGeneration: + """Tests for storage path generation utilities.""" + + def test_generate_token_default_length(self): + """Test token generation with default length.""" + token = generate_token() + assert len(token) == 8 + # All characters should be URL-safe + safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + assert all(c in safe_chars for c in token) + + def test_generate_token_custom_length(self): + """Test token generation with custom length.""" + token = generate_token(12) + assert len(token) == 12 + + def test_generate_token_minimum_length(self): + """Test token generation respects minimum length.""" + token = generate_token(2) # Below minimum + assert len(token) == 4 # Should be clamped to minimum + + def test_generate_token_maximum_length(self): + """Test token generation respects maximum length.""" + token = generate_token(20) # Above maximum + assert len(token) == 16 # Should be clamped to maximum + + def test_generate_token_uniqueness(self): + """Test that generated tokens are unique.""" + tokens = [generate_token() for _ in range(100)] + assert len(set(tokens)) == 100 + + def test_encode_pk_value_integer(self): + """Test encoding integer primary key values.""" + assert encode_pk_value(123) == "123" + assert encode_pk_value(0) == "0" + assert encode_pk_value(-5) == "-5" + + def test_encode_pk_value_string(self): + """Test encoding string primary key values.""" + assert encode_pk_value("simple") == "simple" + assert encode_pk_value("test_value") == "test_value" + + def test_encode_pk_value_unsafe_chars(self): + """Test encoding strings with unsafe characters.""" + # Slash should be URL-encoded + result = encode_pk_value("path/to/file") + assert "/" not in result or result == "path%2Fto%2Ffile" + + def test_build_object_path_basic(self): + """Test basic object path building.""" + path, token = build_object_path( + schema="myschema", + table="MyTable", + field="data_file", + primary_key={"id": 123}, + ext=".dat", + ) + assert "myschema" in path + assert "MyTable" in path + assert "objects" in path + assert "id=123" in path + assert "data_file_" in path + assert path.endswith(".dat") + assert len(token) == 8 + + def test_build_object_path_no_extension(self): + """Test object path building without extension.""" + path, token = build_object_path( + schema="myschema", + table="MyTable", + field="data_folder", + primary_key={"id": 456}, + ext=None, + ) + assert not path.endswith(".") + assert "data_folder_" in path + + def test_build_object_path_multiple_pk(self): + """Test object path with multiple primary key attributes.""" + path, token = build_object_path( + schema="myschema", + table="MyTable", + field="raw_data", + primary_key={"subject_id": 1, "session_id": 2}, + ext=".zarr", + ) + assert "subject_id=1" in path + assert "session_id=2" in path + + def test_build_object_path_with_partition(self): + """Test object path with partition pattern.""" + path, token = build_object_path( + schema="myschema", + table="MyTable", + field="data", + primary_key={"subject_id": 1, "session_id": 2}, + ext=".dat", + partition_pattern="{subject_id}", + ) + # subject_id should be at the beginning due to partition + assert path.startswith("subject_id=1") + + +class TestObjectRef: + """Tests for ObjectRef class.""" + + def test_from_json_string(self): + """Test creating ObjectRef from JSON string.""" + json_str = json.dumps({ + "path": "schema/Table/objects/id=1/data_abc123.dat", + "size": 1024, + "hash": None, + "ext": ".dat", + "is_dir": False, + "timestamp": "2025-01-15T10:30:00+00:00", + }) + obj = ObjectRef.from_json(json_str) + assert obj.path == "schema/Table/objects/id=1/data_abc123.dat" + assert obj.size == 1024 + assert obj.hash is None + assert obj.ext == ".dat" + assert obj.is_dir is False + + def test_from_json_dict(self): + """Test creating ObjectRef from dict.""" + data = { + "path": "schema/Table/objects/id=1/data_abc123.zarr", + "size": 5678, + "hash": None, + "ext": ".zarr", + "is_dir": True, + "timestamp": "2025-01-15T10:30:00+00:00", + "item_count": 42, + } + obj = ObjectRef.from_json(data) + assert obj.path == "schema/Table/objects/id=1/data_abc123.zarr" + assert obj.size == 5678 + assert obj.is_dir is True + assert obj.item_count == 42 + + def test_to_json(self): + """Test converting ObjectRef to JSON dict.""" + from datetime import datetime, timezone + + obj = ObjectRef( + path="schema/Table/objects/id=1/data.dat", + size=1024, + hash=None, + ext=".dat", + is_dir=False, + timestamp=datetime(2025, 1, 15, 10, 30, tzinfo=timezone.utc), + ) + data = obj.to_json() + assert data["path"] == "schema/Table/objects/id=1/data.dat" + assert data["size"] == 1024 + assert data["is_dir"] is False + + def test_repr_file(self): + """Test string representation for file.""" + from datetime import datetime, timezone + + obj = ObjectRef( + path="test/path.dat", + size=1024, + hash=None, + ext=".dat", + is_dir=False, + timestamp=datetime.now(timezone.utc), + ) + assert "file" in repr(obj) + assert "test/path.dat" in repr(obj) + + def test_repr_folder(self): + """Test string representation for folder.""" + from datetime import datetime, timezone + + obj = ObjectRef( + path="test/folder.zarr", + size=5678, + hash=None, + ext=".zarr", + is_dir=True, + timestamp=datetime.now(timezone.utc), + ) + assert "folder" in repr(obj) + + def test_str(self): + """Test str() returns path.""" + from datetime import datetime, timezone + + obj = ObjectRef( + path="my/path/to/data.dat", + size=100, + hash=None, + ext=".dat", + is_dir=False, + timestamp=datetime.now(timezone.utc), + ) + assert str(obj) == "my/path/to/data.dat" + + +class TestObjectInsertFile: + """Tests for inserting files with object type.""" + + def test_insert_file(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test inserting a file.""" + table = ObjectFile() + + # Create a test file + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "test_data.dat") + data = os.urandom(1024) + with test_file.open("wb") as f: + f.write(data) + + # Insert the file + table.insert1({"file_id": 1, "data_file": str(test_file)}) + + # Verify record was inserted + assert len(table) == 1 + + # Cleanup + table.delete() + + def test_insert_file_with_extension(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test that file extension is preserved.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "data.csv") + test_file.write_text("a,b,c\n1,2,3\n") + + table.insert1({"file_id": 2, "data_file": str(test_file)}) + + # Fetch and check extension in metadata + record = table.fetch1() + obj = record["data_file"] + assert obj.ext == ".csv" + + table.delete() + + def test_insert_file_nonexistent(self, schema_obj, mock_object_storage): + """Test that inserting nonexistent file raises error.""" + table = ObjectFile() + + with pytest.raises(dj.DataJointError, match="not found"): + table.insert1({"file_id": 3, "data_file": "/nonexistent/path/file.dat"}) + + +class TestObjectInsertFolder: + """Tests for inserting folders with object type.""" + + def test_insert_folder(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test inserting a folder.""" + table = ObjectFolder() + + # Create a test folder with files + source_folder = tmpdir_factory.mktemp("source") + data_folder = Path(source_folder, "data_folder") + data_folder.mkdir() + + # Add some files + (data_folder / "file1.txt").write_text("content1") + (data_folder / "file2.txt").write_text("content2") + subdir = data_folder / "subdir" + subdir.mkdir() + (subdir / "file3.txt").write_text("content3") + + # Insert the folder + table.insert1({"folder_id": 1, "data_folder": str(data_folder)}) + + assert len(table) == 1 + + # Fetch and verify + record = table.fetch1() + obj = record["data_folder"] + assert obj.is_dir is True + assert obj.item_count == 3 # 3 files + + table.delete() + + +class TestObjectInsertStream: + """Tests for inserting from streams with object type.""" + + def test_insert_stream(self, schema_obj, mock_object_storage): + """Test inserting from a stream.""" + table = ObjectFile() + + # Create a BytesIO stream + data = b"This is test data from a stream" + stream = io.BytesIO(data) + + # Insert with extension and stream tuple + table.insert1({"file_id": 10, "data_file": (".txt", stream)}) + + assert len(table) == 1 + + # Fetch and verify extension + record = table.fetch1() + obj = record["data_file"] + assert obj.ext == ".txt" + assert obj.size == len(data) + + table.delete() + + +class TestObjectFetch: + """Tests for fetching object type attributes.""" + + def test_fetch_returns_objectref(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test that fetch returns ObjectRef.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "test.dat") + test_file.write_bytes(os.urandom(512)) + + table.insert1({"file_id": 20, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + assert isinstance(obj, ObjectRef) + assert obj.size == 512 + assert obj.is_dir is False + + table.delete() + + def test_fetch_metadata_no_io(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test that accessing metadata does not perform I/O.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "test.dat") + test_file.write_bytes(os.urandom(256)) + + table.insert1({"file_id": 21, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + # These should all work without I/O + assert obj.path is not None + assert obj.size == 256 + assert obj.ext == ".dat" + assert obj.is_dir is False + assert obj.timestamp is not None + + table.delete() + + +class TestObjectRefOperations: + """Tests for ObjectRef file operations.""" + + def test_read_file(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test reading file content via ObjectRef.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "readable.dat") + original_data = os.urandom(128) + test_file.write_bytes(original_data) + + table.insert1({"file_id": 30, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + # Read content + content = obj.read() + assert content == original_data + + table.delete() + + def test_open_file(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test opening file via ObjectRef.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "openable.txt") + test_file.write_text("Hello, World!") + + table.insert1({"file_id": 31, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + # Open and read + with obj.open(mode="rb") as f: + content = f.read() + assert content == b"Hello, World!" + + table.delete() + + def test_download_file(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test downloading file via ObjectRef.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "downloadable.dat") + original_data = os.urandom(256) + test_file.write_bytes(original_data) + + table.insert1({"file_id": 32, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + # Download to new location + download_folder = tmpdir_factory.mktemp("download") + local_path = obj.download(download_folder) + + assert Path(local_path).exists() + assert Path(local_path).read_bytes() == original_data + + table.delete() + + def test_exists(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test exists() method.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "exists.dat") + test_file.write_bytes(b"data") + + table.insert1({"file_id": 33, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + assert obj.exists() is True + + table.delete() + + +class TestObjectRefFolderOperations: + """Tests for ObjectRef folder operations.""" + + def test_listdir(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test listing folder contents.""" + table = ObjectFolder() + + source_folder = tmpdir_factory.mktemp("source") + data_folder = Path(source_folder, "listable") + data_folder.mkdir() + (data_folder / "a.txt").write_text("a") + (data_folder / "b.txt").write_text("b") + (data_folder / "c.txt").write_text("c") + + table.insert1({"folder_id": 40, "data_folder": str(data_folder)}) + + record = table.fetch1() + obj = record["data_folder"] + + contents = obj.listdir() + assert len(contents) == 3 + assert "a.txt" in contents + assert "b.txt" in contents + assert "c.txt" in contents + + table.delete() + + def test_walk(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test walking folder tree.""" + table = ObjectFolder() + + source_folder = tmpdir_factory.mktemp("source") + data_folder = Path(source_folder, "walkable") + data_folder.mkdir() + (data_folder / "root.txt").write_text("root") + subdir = data_folder / "subdir" + subdir.mkdir() + (subdir / "nested.txt").write_text("nested") + + table.insert1({"folder_id": 41, "data_folder": str(data_folder)}) + + record = table.fetch1() + obj = record["data_folder"] + + # Collect walk results + walk_results = list(obj.walk()) + assert len(walk_results) >= 1 + + table.delete() + + def test_open_subpath(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test opening file within folder using subpath.""" + table = ObjectFolder() + + source_folder = tmpdir_factory.mktemp("source") + data_folder = Path(source_folder, "subpathable") + data_folder.mkdir() + (data_folder / "inner.txt").write_text("inner content") + + table.insert1({"folder_id": 42, "data_folder": str(data_folder)}) + + record = table.fetch1() + obj = record["data_folder"] + + with obj.open("inner.txt", mode="rb") as f: + content = f.read() + assert content == b"inner content" + + table.delete() + + def test_read_on_folder_raises(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test that read() on folder raises error.""" + table = ObjectFolder() + + source_folder = tmpdir_factory.mktemp("source") + data_folder = Path(source_folder, "folder") + data_folder.mkdir() + (data_folder / "file.txt").write_text("content") + + table.insert1({"folder_id": 43, "data_folder": str(data_folder)}) + + record = table.fetch1() + obj = record["data_folder"] + + with pytest.raises(dj.DataJointError, match="Cannot read"): + obj.read() + + table.delete() + + def test_listdir_on_file_raises(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test that listdir() on file raises error.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "file.dat") + test_file.write_bytes(b"data") + + table.insert1({"file_id": 44, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + with pytest.raises(dj.DataJointError, match="Cannot listdir"): + obj.listdir() + + table.delete() + + +class TestObjectMultiple: + """Tests for tables with multiple object attributes.""" + + def test_multiple_objects(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test inserting multiple object attributes.""" + table = ObjectMultiple() + + source_folder = tmpdir_factory.mktemp("source") + raw_file = Path(source_folder, "raw.dat") + raw_file.write_bytes(os.urandom(100)) + processed_file = Path(source_folder, "processed.dat") + processed_file.write_bytes(os.urandom(200)) + + table.insert1({ + "record_id": 1, + "raw_data": str(raw_file), + "processed": str(processed_file), + }) + + record = table.fetch1() + raw_obj = record["raw_data"] + processed_obj = record["processed"] + + assert raw_obj.size == 100 + assert processed_obj.size == 200 + assert raw_obj.path != processed_obj.path + + table.delete() + + +class TestObjectWithOtherAttributes: + """Tests for object type mixed with other attributes.""" + + def test_object_with_other(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test table with object and other attribute types.""" + table = ObjectWithOther() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "data.bin") + test_file.write_bytes(os.urandom(64)) + + table.insert1({ + "subject_id": 1, + "session_id": 1, + "name": "Test Session", + "data_file": str(test_file), + "notes": "Some notes here", + }) + + record = table.fetch1() + assert record["name"] == "Test Session" + assert record["notes"] == "Some notes here" + assert isinstance(record["data_file"], ObjectRef) + assert record["data_file"].size == 64 + + table.delete() + + +class TestObjectVerify: + """Tests for ObjectRef verification.""" + + def test_verify_file(self, schema_obj, mock_object_storage, tmpdir_factory): + """Test verifying file integrity.""" + table = ObjectFile() + + source_folder = tmpdir_factory.mktemp("source") + test_file = Path(source_folder, "verifiable.dat") + test_file.write_bytes(os.urandom(128)) + + table.insert1({"file_id": 50, "data_file": str(test_file)}) + + record = table.fetch1() + obj = record["data_file"] + + # Should not raise + assert obj.verify() is True + + table.delete() + + +class TestStagedInsert: + """Tests for staged insert operations.""" + + def test_staged_insert_basic(self, schema_obj, mock_object_storage): + """Test basic staged insert.""" + table = ObjectFile() + + with table.staged_insert1 as staged: + staged.rec["file_id"] = 60 + + # Write directly to storage + with staged.open("data_file", ".dat") as f: + f.write(b"staged data content") + + # No need to assign - metadata computed on exit + + # Verify record was inserted + assert len(table) == 1 + record = table.fetch1() + obj = record["data_file"] + assert obj.ext == ".dat" + + table.delete() + + def test_staged_insert_exception_cleanup(self, schema_obj, mock_object_storage): + """Test that staged insert cleans up on exception.""" + table = ObjectFile() + + try: + with table.staged_insert1 as staged: + staged.rec["file_id"] = 61 + + with staged.open("data_file", ".dat") as f: + f.write(b"will be cleaned up") + + raise ValueError("Simulated error") + except ValueError: + pass + + # No record should be inserted + assert len(table) == 0 + + def test_staged_insert_store_method(self, schema_obj, mock_object_storage): + """Test staged insert store() method returns FSMap.""" + import fsspec + + table = ObjectFile() + + with table.staged_insert1 as staged: + staged.rec["file_id"] = 62 + + store = staged.store("data_file", ".zarr") + assert isinstance(store, fsspec.FSMap) + + # Write some data + store["test_key"] = b"test_value" + + assert len(table) == 1 + + table.delete() + + def test_staged_insert_fs_property(self, schema_obj, mock_object_storage): + """Test staged insert fs property returns filesystem.""" + import fsspec + + table = ObjectFile() + + with table.staged_insert1 as staged: + staged.rec["file_id"] = 63 + + fs = staged.fs + assert isinstance(fs, fsspec.AbstractFileSystem) + + # Just open and write to test fs works + with staged.open("data_file", ".txt") as f: + f.write(b"test") + + table.delete() + + def test_staged_insert_missing_pk_raises(self, schema_obj, mock_object_storage): + """Test that staged insert raises if PK not set before store().""" + table = ObjectFile() + + with pytest.raises(dj.DataJointError, match="Primary key"): + with table.staged_insert1 as staged: + # Don't set primary key + staged.store("data_file", ".dat") From 944c9be63de32339675b4bb6e1625764219a1491 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:30:07 +0000 Subject: [PATCH 073/219] Fix E402: move schema_object import to top of file --- tests/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 136543fa8..c2f2a5ae9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,7 +21,7 @@ DataJointError, ) -from . import schema, schema_advanced, schema_external, schema_simple +from . import schema, schema_advanced, schema_external, schema_object, schema_simple from . import schema_uuid as schema_uuid_module from . import schema_type_aliases as schema_type_aliases_module @@ -906,7 +906,6 @@ def trash(schema_any): # Object storage fixtures -from . import schema_object @pytest.fixture From 752248c9f983aff40433db0f394b30a6c192b39d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:31:23 +0000 Subject: [PATCH 074/219] Fix unused imports (ruff lint) --- tests/test_object.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_object.py b/tests/test_object.py index decd1acae..b5e3d22b5 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -18,7 +18,7 @@ import pytest import datajoint as dj -from datajoint.objectref import ObjectRef, IntegrityError +from datajoint.objectref import ObjectRef from datajoint.storage import build_object_path, generate_token, encode_pk_value from .schema_object import ObjectFile, ObjectFolder, ObjectMultiple, ObjectWithOther From 7ef4e61e70d4e432580b6c903d97c88da632c180 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Dec 2025 01:33:35 +0000 Subject: [PATCH 075/219] Fix ruff-format: add blank lines after local imports --- tests/test_object.py | 46 +++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/test_object.py b/tests/test_object.py index b5e3d22b5..8cfd5d896 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -132,14 +132,16 @@ class TestObjectRef: def test_from_json_string(self): """Test creating ObjectRef from JSON string.""" - json_str = json.dumps({ - "path": "schema/Table/objects/id=1/data_abc123.dat", - "size": 1024, - "hash": None, - "ext": ".dat", - "is_dir": False, - "timestamp": "2025-01-15T10:30:00+00:00", - }) + json_str = json.dumps( + { + "path": "schema/Table/objects/id=1/data_abc123.dat", + "size": 1024, + "hash": None, + "ext": ".dat", + "is_dir": False, + "timestamp": "2025-01-15T10:30:00+00:00", + } + ) obj = ObjectRef.from_json(json_str) assert obj.path == "schema/Table/objects/id=1/data_abc123.dat" assert obj.size == 1024 @@ -581,11 +583,13 @@ def test_multiple_objects(self, schema_obj, mock_object_storage, tmpdir_factory) processed_file = Path(source_folder, "processed.dat") processed_file.write_bytes(os.urandom(200)) - table.insert1({ - "record_id": 1, - "raw_data": str(raw_file), - "processed": str(processed_file), - }) + table.insert1( + { + "record_id": 1, + "raw_data": str(raw_file), + "processed": str(processed_file), + } + ) record = table.fetch1() raw_obj = record["raw_data"] @@ -609,13 +613,15 @@ def test_object_with_other(self, schema_obj, mock_object_storage, tmpdir_factory test_file = Path(source_folder, "data.bin") test_file.write_bytes(os.urandom(64)) - table.insert1({ - "subject_id": 1, - "session_id": 1, - "name": "Test Session", - "data_file": str(test_file), - "notes": "Some notes here", - }) + table.insert1( + { + "subject_id": 1, + "session_id": 1, + "name": "Test Session", + "data_file": str(test_file), + "notes": "Some notes here", + } + ) record = table.fetch1() assert record["name"] == "Test Session" From f9e749674d45c084495aca6d8d8997fab7258858 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 21 Dec 2025 18:58:29 -0600 Subject: [PATCH 076/219] "strongly-typed" -> "type-checked" Json in documentation. Co-authored-by: Davis Bennett --- docs/src/client/settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/client/settings.md b/docs/src/client/settings.md index d9fd468a2..a05369bb9 100644 --- a/docs/src/client/settings.md +++ b/docs/src/client/settings.md @@ -1,6 +1,6 @@ # Configuration Settings -DataJoint uses a strongly-typed configuration system built on [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). +DataJoint uses a type-checked configuration system built on [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). ## Configuration Sources From d68ea68832c6f4911732d6eb429ac0c7479cf756 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 14:18:49 +0000 Subject: [PATCH 077/219] feat: add bool/boolean type alias Add bool and boolean as type aliases mapping to MySQL tinyint. Update tests and documentation accordingly. --- docs/src/design/tables/attributes.md | 2 ++ src/datajoint/declare.py | 3 ++- tests/schema_type_aliases.py | 1 + tests/test_type_aliases.py | 7 +++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/src/design/tables/attributes.md b/docs/src/design/tables/attributes.md index 9363e527f..1b47bfd6b 100644 --- a/docs/src/design/tables/attributes.md +++ b/docs/src/design/tables/attributes.md @@ -85,6 +85,7 @@ libraries, making table definitions more readable and explicit about data precis | Alias | MySQL Type | Description | |-------|------------|-------------| +| `bool` / `boolean` | `tinyint` | Boolean value (0 or 1) | | `int8` | `tinyint` | 8-bit signed integer (-128 to 127) | | `uint8` | `tinyint unsigned` | 8-bit unsigned integer (0 to 255) | | `int16` | `smallint` | 16-bit signed integer (-32,768 to 32,767) | @@ -108,6 +109,7 @@ class Measurement(dj.Manual): precise_value : float64 # double-precision measurement sample_count : uint32 # unsigned 32-bit counter sensor_flags : uint8 # 8-bit status flags + is_valid : bool # boolean flag """ ``` diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index c1a22f0ca..8254ddfd0 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -28,6 +28,7 @@ "UINT16": "smallint unsigned", "INT8": "tinyint", "UINT8": "tinyint unsigned", + "BOOL": "tinyint", } MAX_TABLE_NAME_LENGTH = 64 CONSTANT_LITERALS = { @@ -50,6 +51,7 @@ UINT16=r"uint16$", INT8=r"int8$", UINT8=r"uint8$", + BOOL=r"bool(ean)?$", # aliased to tinyint # Native MySQL types INTEGER=r"((tiny|small|medium|big|)int|integer)(\s*\(.+\))?(\s+unsigned)?(\s+auto_increment)?|serial$", DECIMAL=r"(decimal|numeric)(\s*\(.+\))?(\s+unsigned)?$", @@ -57,7 +59,6 @@ STRING=r"(var)?char\s*\(.+\)$", JSON=r"json$", ENUM=r"enum\s*\(.+\)$", - BOOL=r"bool(ean)?$", # aliased to tinyint(1) TEMPORAL=r"(date|datetime|time|timestamp|year)(\s*\(.+\))?$", INTERNAL_BLOB=r"(tiny|small|medium|long|)blob$", EXTERNAL_BLOB=r"blob@(?P[a-z][\-\w]*)$", diff --git a/tests/schema_type_aliases.py b/tests/schema_type_aliases.py index cdd558868..eb586de5d 100644 --- a/tests/schema_type_aliases.py +++ b/tests/schema_type_aliases.py @@ -22,6 +22,7 @@ class TypeAliasTable(dj.Manual): val_uint16 : uint16 # 16-bit unsigned integer val_int8 : int8 # 8-bit signed integer val_uint8 : uint8 # 8-bit unsigned integer + val_bool : bool # boolean value """ diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index 436d608bf..95b0bbeed 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -25,6 +25,8 @@ class TestTypeAliasPatterns: ("uint16", "UINT16"), ("int8", "INT8"), ("uint8", "UINT8"), + ("bool", "BOOL"), + ("boolean", "BOOL"), ], ) def test_type_alias_pattern_matching(self, alias, expected_category): @@ -47,6 +49,8 @@ def test_type_alias_pattern_matching(self, alias, expected_category): ("uint16", "smallint unsigned"), ("int8", "tinyint"), ("uint8", "tinyint unsigned"), + ("bool", "tinyint"), + ("boolean", "tinyint"), ], ) def test_type_alias_mysql_mapping(self, alias, expected_mysql_type): @@ -107,6 +111,7 @@ def test_heading_preserves_type_aliases(self, schema_type_aliases): assert "uint16" in heading_str assert "int8" in heading_str assert "uint8" in heading_str + assert "bool" in heading_str class TestTypeAliasInsertFetch: @@ -129,6 +134,7 @@ def test_insert_and_fetch(self, schema_type_aliases): val_uint16=65535, # max uint16 val_int8=127, # max int8 val_uint8=255, # max uint8 + val_bool=1, # boolean true ) table.insert1(test_data) @@ -145,6 +151,7 @@ def test_insert_and_fetch(self, schema_type_aliases): assert fetched["val_uint16"] == test_data["val_uint16"] assert fetched["val_int8"] == test_data["val_int8"] assert fetched["val_uint8"] == test_data["val_uint8"] + assert fetched["val_bool"] == test_data["val_bool"] def test_insert_primary_key_with_aliases(self, schema_type_aliases): """Test using type aliases in primary key.""" From e20281856521a401b621af9a27981f5dbfef0a9b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 14:20:02 +0000 Subject: [PATCH 078/219] refactor: simplify bool type alias to only support 'bool' --- docs/src/design/tables/attributes.md | 2 +- src/datajoint/declare.py | 2 +- tests/test_type_aliases.py | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/src/design/tables/attributes.md b/docs/src/design/tables/attributes.md index 1b47bfd6b..b68f552e5 100644 --- a/docs/src/design/tables/attributes.md +++ b/docs/src/design/tables/attributes.md @@ -85,7 +85,7 @@ libraries, making table definitions more readable and explicit about data precis | Alias | MySQL Type | Description | |-------|------------|-------------| -| `bool` / `boolean` | `tinyint` | Boolean value (0 or 1) | +| `bool` | `tinyint` | Boolean value (0 or 1) | | `int8` | `tinyint` | 8-bit signed integer (-128 to 127) | | `uint8` | `tinyint unsigned` | 8-bit unsigned integer (0 to 255) | | `int16` | `smallint` | 16-bit signed integer (-32,768 to 32,767) | diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index 8254ddfd0..e21193e50 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -51,7 +51,7 @@ UINT16=r"uint16$", INT8=r"int8$", UINT8=r"uint8$", - BOOL=r"bool(ean)?$", # aliased to tinyint + BOOL=r"bool$", # aliased to tinyint # Native MySQL types INTEGER=r"((tiny|small|medium|big|)int|integer)(\s*\(.+\))?(\s+unsigned)?(\s+auto_increment)?|serial$", DECIMAL=r"(decimal|numeric)(\s*\(.+\))?(\s+unsigned)?$", diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index 95b0bbeed..019b69498 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -26,7 +26,6 @@ class TestTypeAliasPatterns: ("int8", "INT8"), ("uint8", "UINT8"), ("bool", "BOOL"), - ("boolean", "BOOL"), ], ) def test_type_alias_pattern_matching(self, alias, expected_category): @@ -50,7 +49,6 @@ def test_type_alias_pattern_matching(self, alias, expected_category): ("int8", "tinyint"), ("uint8", "tinyint unsigned"), ("bool", "tinyint"), - ("boolean", "tinyint"), ], ) def test_type_alias_mysql_mapping(self, alias, expected_mysql_type): From 15418c339cde649adf85bf819869440c512ec1d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 14:58:42 +0000 Subject: [PATCH 079/219] Address Zarr reviewer feedback: optional metadata fields - Make size field optional (nullable) for large hierarchical data - Add Performance Considerations section documenting expensive operations - Add Extension Field section clarifying ext is a tooling hint - Add Storage Access Architecture section noting fsspec pluggability - Add comprehensive Zarr and Large Hierarchical Data section - Update ObjectRef dataclass to support optional size - Add test for Zarr-style JSON with null size --- docs/src/design/tables/file-type-spec.md | 134 ++++++++++++++++++++++- src/datajoint/objectref.py | 24 ++-- tests/test_object.py | 18 +++ 3 files changed, 163 insertions(+), 13 deletions(-) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index dc1eae987..474d18c1f 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -288,18 +288,50 @@ The `object` type is stored as a `JSON` column in MySQL containing: } ``` +**Zarr example (large dataset, metadata fields omitted for performance):** +```json +{ + "path": "my_schema/Recording/objects/subject_id=123/session_id=45/neural_data_kM3nP2qR.zarr", + "size": null, + "hash": null, + "ext": ".zarr", + "is_dir": true, + "timestamp": "2025-01-15T10:30:00Z" +} +``` + ### JSON Schema | Field | Type | Required | Description | |-------|------|----------|-------------| | `path` | string | Yes | Full path/key within storage backend (includes token) | -| `size` | integer | Yes | Total size in bytes (sum for folders) | +| `size` | integer/null | No | Total size in bytes (sum for folders), or null if not computed. See [Performance Considerations](#performance-considerations). | | `hash` | string/null | Yes | Content hash with algorithm prefix, or null (default) | -| `ext` | string/null | Yes | File extension (e.g., `.dat`, `.zarr`) or null | -| `is_dir` | boolean | Yes | True if stored content is a directory | +| `ext` | string/null | Yes | File extension as tooling hint (e.g., `.dat`, `.zarr`) or null. See [Extension Field](#extension-field). | +| `is_dir` | boolean | Yes | True if stored content is a directory/key-prefix (e.g., Zarr store) | | `timestamp` | string | Yes | ISO 8601 upload timestamp | | `mime_type` | string | No | MIME type (files only, auto-detected from extension) | -| `item_count` | integer | No | Number of files (folders only) | +| `item_count` | integer | No | Number of files (folders only), or null if not computed. See [Performance Considerations](#performance-considerations). | + +### Extension Field + +The `ext` field is a **tooling hint** that preserves the original file extension or provides a conventional suffix for directory-based formats. It is: + +- **Not a content-type declaration**: Unlike `mime_type`, it does not attempt to describe the internal content format +- **Useful for tooling**: Enables file browsers, IDEs, and other tools to display appropriate icons or suggest applications +- **Conventional for formats like Zarr**: The `.zarr` extension is recognized by the ecosystem even though a Zarr store contains mixed content (JSON metadata + binary chunks) + +For single files, `ext` is extracted from the source filename. For staged inserts (like Zarr), it can be explicitly provided. + +### Performance Considerations + +For large hierarchical data like Zarr stores, computing certain metadata can be expensive: + +- **`size`**: Requires listing all objects and summing their sizes. For stores with millions of chunks, this can take minutes or hours. +- **`item_count`**: Requires listing all objects. Same performance concern as `size`. +- **`hash`**: Requires reading all content. Explicitly not supported for staged inserts. + +**These fields are optional** and default to `null` for staged inserts. Users can explicitly request computation when needed, understanding the performance implications. ### Content Hashing @@ -996,6 +1028,20 @@ gcs = ["gcsfs"] azure = ["adlfs"] ``` +### Storage Access Architecture + +The `object` type separates **data declaration** (the JSON metadata stored in the database) from **storage access** (the library used to read/write objects): + +- **Data declaration**: The JSON schema (path, size, hash, etc.) is a pure data structure with no library dependencies +- **Storage access**: Currently uses `fsspec` as the default accessor, but the architecture supports alternative backends + +**Why this matters**: While `fsspec` is a mature and widely-used library, alternatives like [`obstore`](https://github.com/developmentseed/obstore) offer performance advantages for certain workloads. By keeping the data model independent of the access library, future versions can support pluggable storage accessors without schema changes. + +**Current implementation**: The `ObjectRef` class provides fsspec-based accessors (`fs`, `store` properties). Future versions may add: +- Pluggable accessor interface +- Alternative backends (obstore, custom implementations) +- Backend selection per-operation or per-configuration + ## Comparison with Existing Types | Feature | `attach@store` | `filepath@store` | `object` | @@ -1073,6 +1119,86 @@ Each record owns its file exclusively. There is no deduplication or reference co - `object` type is additive - new tables only - Future: Migration utilities to convert existing external storage +## Zarr and Large Hierarchical Data + +The `object` type is designed with Zarr and similar hierarchical data formats (HDF5 via kerchunk, TileDB) in mind. This section provides guidance for these use cases. + +### Recommended Workflow + +For large Zarr stores, use **staged insert** to write directly to object storage: + +```python +import zarr +import numpy as np + +with Recording.staged_insert1 as staged: + staged.rec['subject_id'] = 123 + staged.rec['session_id'] = 45 + + # Write Zarr directly to object storage + store = staged.store('neural_data', '.zarr') + root = zarr.open(store, mode='w') + root.create_dataset('spikes', shape=(1000000, 384), chunks=(10000, 384), dtype='f4') + + # Stream data without local intermediate copy + for i, chunk in enumerate(acquisition_stream): + root['spikes'][i*10000:(i+1)*10000] = chunk + + staged.rec['neural_data'] = root + +# Metadata recorded, no expensive size/hash computation +``` + +### JSON Metadata for Zarr + +For Zarr stores, the recommended JSON metadata omits expensive-to-compute fields: + +```json +{ + "path": "schema/Recording/objects/subject_id=123/session_id=45/neural_data_kM3nP2qR.zarr", + "size": null, + "hash": null, + "ext": ".zarr", + "is_dir": true, + "timestamp": "2025-01-15T10:30:00Z" +} +``` + +**Field notes for Zarr:** +- **`size`**: Set to `null` - computing total size requires listing all chunks +- **`hash`**: Always `null` for staged inserts - no merkle tree support currently +- **`ext`**: Set to `.zarr` as a conventional tooling hint +- **`is_dir`**: Set to `true` - Zarr stores are key prefixes (logical directories) +- **`item_count`**: Omitted - counting chunks is expensive and rarely useful +- **`mime_type`**: Omitted - Zarr contains mixed content types + +### Reading Zarr Data + +The `ObjectRef` provides direct access compatible with Zarr and xarray: + +```python +record = Recording.fetch1() +obj_ref = record['neural_data'] + +# Direct Zarr access +z = zarr.open(obj_ref.store, mode='r') +print(z['spikes'].shape) + +# xarray integration +ds = xr.open_zarr(obj_ref.store) + +# Dask integration (lazy loading) +import dask.array as da +arr = da.from_zarr(obj_ref.store, component='spikes') +``` + +### Performance Tips + +1. **Use chunked writes**: Write data in chunks that match your Zarr chunk size +2. **Avoid metadata computation**: Let `size` and `item_count` default to `null` +3. **Use appropriate chunk sizes**: Balance between too many small files (overhead) and too few large files (memory) +4. **Consider compression**: Configure Zarr compression (blosc, zstd) to reduce storage costs + ## Future Extensions - [ ] Compression options (gzip, lz4, zstd) diff --git a/src/datajoint/objectref.py b/src/datajoint/objectref.py index f3cfffef8..cc1437178 100644 --- a/src/datajoint/objectref.py +++ b/src/datajoint/objectref.py @@ -35,17 +35,20 @@ class ObjectRef: Attributes: path: Full path/key within storage backend (includes token) - size: Total size in bytes (sum for folders) + size: Total size in bytes (sum for folders), or None if not computed. + For large hierarchical data like Zarr stores, size computation can + be expensive and is optional. hash: Content hash with algorithm prefix, or None if not computed - ext: File extension (e.g., ".dat", ".zarr") or None - is_dir: True if stored content is a directory + ext: File extension as tooling hint (e.g., ".dat", ".zarr") or None. + This is a conventional suffix for tooling, not a content-type declaration. + is_dir: True if stored content is a directory/key-prefix (e.g., Zarr store) timestamp: ISO 8601 upload timestamp mime_type: MIME type (files only, auto-detected from extension) - item_count: Number of files (folders only) + item_count: Number of files (folders only), or None if not computed """ path: str - size: int + size: int | None hash: str | None ext: str | None is_dir: bool @@ -307,10 +310,13 @@ def _verify_file(self) -> bool: if not self._backend.exists(self.path): raise IntegrityError(f"File does not exist: {self.path}") - # Check size - actual_size = self._backend.size(self.path) - if actual_size != self.size: - raise IntegrityError(f"Size mismatch for {self.path}: expected {self.size}, got {actual_size}") + # Check size if available + if self.size is not None: + actual_size = self._backend.size(self.path) + if actual_size != self.size: + raise IntegrityError( + f"Size mismatch for {self.path}: expected {self.size}, got {actual_size}" + ) # Check hash if available if self.hash: diff --git a/tests/test_object.py b/tests/test_object.py index 8cfd5d896..8b8a34056 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -166,6 +166,24 @@ def test_from_json_dict(self): assert obj.is_dir is True assert obj.item_count == 42 + def test_from_json_zarr_style(self): + """Test creating ObjectRef from Zarr-style JSON with null size.""" + data = { + "path": "schema/Recording/objects/id=1/neural_data_abc123.zarr", + "size": None, + "hash": None, + "ext": ".zarr", + "is_dir": True, + "timestamp": "2025-01-15T10:30:00+00:00", + } + obj = ObjectRef.from_json(data) + assert obj.path == "schema/Recording/objects/id=1/neural_data_abc123.zarr" + assert obj.size is None + assert obj.hash is None + assert obj.ext == ".zarr" + assert obj.is_dir is True + assert obj.item_count is None + def test_to_json(self): """Test converting ObjectRef to JSON dict.""" from datetime import datetime, timezone From fb8c0cba6f02a5aee4223c8b949ca0cb874fda0f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 15:16:20 +0000 Subject: [PATCH 080/219] Add Augmented Schema vs External References section Clarifies the architectural distinction between the object type (AUS) and filepath@store (external references) to address reviewer question about multi-cloud scenarios. --- docs/src/design/tables/file-type-spec.md | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/file-type-spec.md index 474d18c1f..40a009875 100644 --- a/docs/src/design/tables/file-type-spec.md +++ b/docs/src/design/tables/file-type-spec.md @@ -23,6 +23,31 @@ Once an object is **finalized** (either via copy-insert or staged-insert complet | **Copy** | Small files, existing data | Local file → copy to storage → insert record | | **Staged** | Large objects, Zarr/HDF5 | Reserve path → write directly to storage → finalize record | +### Augmented Schema vs External References + +The `object` type implements **Augmented Schema (AUS)** — a paradigm where the object store becomes a true extension of the relational database: + +- **DataJoint fully controls** the object store lifecycle +- **Only DataJoint writes** to the object store (users may have direct read access) +- **Tight coupling** between database and object store +- **Joint transaction management** on objects and database records +- **Single backend per pipeline** — all managed objects live together + +This is fundamentally different from **external references**, where DataJoint merely points to user-managed data: + +| Aspect | `object` (Augmented Schema) | `filepath@store` (External Reference) | +|--------|----------------------------|--------------------------------------| +| **Ownership** | DataJoint owns the data | User owns the data | +| **Writes** | Only via DataJoint | User writes directly | +| **Deletion** | DataJoint deletes on record delete | User manages lifecycle | +| **Multi-backend** | Single backend per pipeline | Multiple named stores | +| **Use case** | Pipeline-generated data | Collaborator data, legacy assets | + +**When to use each:** + +- Use `object` for data that DataJoint should own and manage as part of the schema (e.g., processed results, derived datasets) +- Use `filepath@store` for referencing externally-managed data across multiple backends (e.g., collaborator data on different cloud providers, legacy data that shouldn't be moved) + ## Storage Architecture ### Single Storage Backend Per Pipeline From a9447e73a628bf10046d324b1773cbc764984df6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 15:19:11 +0000 Subject: [PATCH 081/219] Rename file-type-spec.md to object-type-spec.md --- docs/src/design/tables/{file-type-spec.md => object-type-spec.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/src/design/tables/{file-type-spec.md => object-type-spec.md} (100%) diff --git a/docs/src/design/tables/file-type-spec.md b/docs/src/design/tables/object-type-spec.md similarity index 100% rename from docs/src/design/tables/file-type-spec.md rename to docs/src/design/tables/object-type-spec.md From 5170ab14f96613dfe2b07badd8a402f7ec3c28ed Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 15:32:09 +0000 Subject: [PATCH 082/219] Fix ruff-format: single line error message --- src/datajoint/objectref.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/datajoint/objectref.py b/src/datajoint/objectref.py index cc1437178..32f7b1669 100644 --- a/src/datajoint/objectref.py +++ b/src/datajoint/objectref.py @@ -314,9 +314,7 @@ def _verify_file(self) -> bool: if self.size is not None: actual_size = self._backend.size(self.path) if actual_size != self.size: - raise IntegrityError( - f"Size mismatch for {self.path}: expected {self.size}, got {actual_size}" - ) + raise IntegrityError(f"Size mismatch for {self.path}: expected {self.size}, got {actual_size}") # Check hash if available if self.hash: From 3e321881d726ddd056e62eea6bd02422ef2dbc68 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 15:41:14 +0000 Subject: [PATCH 083/219] Simplify ExternalTable storage initialization Remove lazy initialization pattern for storage attribute since it was being initialized in __init__ anyway. Storage is now a regular instance attribute instead of a property. --- src/datajoint/external.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/datajoint/external.py b/src/datajoint/external.py index dbb99cae7..6e00a67b9 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -39,7 +39,6 @@ class ExternalTable(Table): def __init__(self, connection, store, database): self.store = store self.spec = config.get_store_spec(store) - self._storage = None self.database = database self._connection = connection self._heading = Heading( @@ -54,7 +53,7 @@ def __init__(self, connection, store, database): if not self.is_declared: self.declare() # Initialize storage backend (validates configuration) - _ = self.storage + self.storage = StorageBackend(self.spec) @property def definition(self): @@ -73,13 +72,6 @@ def definition(self): def table_name(self): return f"{EXTERNAL_TABLE_ROOT}_{self.store}" - @property - def storage(self) -> StorageBackend: - """Get or create the storage backend instance.""" - if self._storage is None: - self._storage = StorageBackend(self.spec) - return self._storage - @property def s3(self): """Deprecated: Use storage property instead.""" From 4e90c1e83dedef767d7eecb53a563199d1bbd6c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 15:57:24 +0000 Subject: [PATCH 084/219] Clarify staged insert compatibility: Zarr/TileDB yes, HDF5 no - HDF5 requires random-access seek/write operations incompatible with object storage's PUT/GET model - Staged inserts work with chunk-based formats (Zarr, TileDB) where each chunk is a separate object - Added compatibility table and HDF5 copy-insert example - Recommend Zarr over HDF5 for cloud-native workflows --- docs/src/design/tables/object-type-spec.md | 33 +++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/src/design/tables/object-type-spec.md b/docs/src/design/tables/object-type-spec.md index 40a009875..2e5514fd6 100644 --- a/docs/src/design/tables/object-type-spec.md +++ b/docs/src/design/tables/object-type-spec.md @@ -21,7 +21,7 @@ Once an object is **finalized** (either via copy-insert or staged-insert complet | Mode | Use Case | Workflow | |------|----------|----------| | **Copy** | Small files, existing data | Local file → copy to storage → insert record | -| **Staged** | Large objects, Zarr/HDF5 | Reserve path → write directly to storage → finalize record | +| **Staged** | Large objects, Zarr, TileDB | Reserve path → write directly to storage → finalize record | ### Augmented Schema vs External References @@ -1144,11 +1144,36 @@ Each record owns its file exclusively. There is no deduplication or reference co - `object` type is additive - new tables only - Future: Migration utilities to convert existing external storage -## Zarr and Large Hierarchical Data +## Zarr, TileDB, and Large Hierarchical Data -The `object` type is designed with Zarr and similar hierarchical data formats (HDF5 via kerchunk, TileDB) in mind. This section provides guidance for these use cases. +The `object` type is designed with **chunk-based formats** like Zarr and TileDB in mind. These formats store each chunk as a separate object, which maps naturally to object storage. -### Recommended Workflow +### Staged Insert Compatibility + +**Staged inserts work with formats that support chunk-based writes:** + +| Format | Staged Insert | Why | +|--------|---------------|-----| +| **Zarr** | ✅ Yes | Each chunk is a separate object | +| **TileDB** | ✅ Yes | Fragment-based storage maps to objects | +| **HDF5** | ❌ No | Single monolithic file requires random-access seek/write | + +**HDF5 limitation**: HDF5 files have internal B-tree structures that require random-access modifications. Object storage only supports full object PUT/GET operations, not partial updates. For HDF5, use **copy insert**: + +```python +# HDF5: Write locally, then copy to object storage +import h5py +import tempfile + +with tempfile.NamedTemporaryFile(suffix='.h5', delete=False) as f: + with h5py.File(f.name, 'w') as h5: + h5.create_dataset('data', data=large_array) + Recording.insert1({..., 'data_file': f.name}) +``` + +For cloud-native workflows with large arrays, **Zarr is recommended** over HDF5. + +### Recommended Workflow (Zarr) For large Zarr stores, use **staged insert** to write directly to object storage: From 5a727d2877f783f349f7cb0364c9937ad44ae58f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 16:07:33 +0000 Subject: [PATCH 085/219] Add remote URL support for copy insert - Add is_remote_url() and parse_remote_url() helpers to storage.py - Add copy_from_url() method to StorageBackend for remote-to-managed copies - Add source_exists(), source_is_directory(), get_source_size() helpers - Support s3://, gs://, az://, http://, https:// protocols - Update spec with Remote URL Support section and examples - Update object.md with "Inserting from Remote URLs" section - Update insert.md with remote URL examples - Add TestRemoteURLSupport test class --- docs/src/design/tables/object-type-spec.md | 45 ++++- docs/src/design/tables/object.md | 33 +++- docs/src/manipulation/insert.md | 24 ++- src/datajoint/storage.py | 198 +++++++++++++++++++++ tests/test_object.py | 91 ++++++++++ 5 files changed, 380 insertions(+), 11 deletions(-) diff --git a/docs/src/design/tables/object-type-spec.md b/docs/src/design/tables/object-type-spec.md index 2e5514fd6..dea83c5f4 100644 --- a/docs/src/design/tables/object-type-spec.md +++ b/docs/src/design/tables/object-type-spec.md @@ -584,12 +584,13 @@ Each insert stores a separate copy of the file, even if identical content was pr At insert time, the `object` attribute accepts: -1. **File path** (string or `Path`): Path to an existing file (extension extracted) -2. **Folder path** (string or `Path`): Path to an existing directory -3. **Tuple of (ext, stream)**: File-like object with explicit extension +1. **Local file path** (string or `Path`): Path to an existing local file (extension extracted) +2. **Local folder path** (string or `Path`): Path to an existing local directory +3. **Remote URL** (string): URL to remote file or folder (`s3://`, `gs://`, `az://`, `http://`, `https://`) +4. **Tuple of (ext, stream)**: File-like object with explicit extension ```python -# From file path - extension (.dat) extracted from source +# From local file path - extension (.dat) extracted from source Recording.insert1({ "subject_id": 123, "session_id": 45, @@ -597,7 +598,7 @@ Recording.insert1({ }) # Stored as: raw_data_Ax7bQ2kM.dat -# From folder path - no extension +# From local folder path - no extension Recording.insert1({ "subject_id": 123, "session_id": 45, @@ -605,6 +606,22 @@ Recording.insert1({ }) # Stored as: raw_data_pL9nR4wE/ +# From remote URL - copies from source to managed storage +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "s3://source-bucket/path/to/data.dat" +}) +# Stored as: raw_data_kM3nP2qR.dat + +# From remote Zarr store (e.g., collaborator data on GCS) +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "neural_data": "gs://collaborator-bucket/shared/experiment.zarr" +}) +# Copied to managed storage as: neural_data_pL9nR4wE.zarr + # From stream with explicit extension with open("/local/path/data.bin", "rb") as f: Recording.insert1({ @@ -612,9 +629,25 @@ with open("/local/path/data.bin", "rb") as f: "session_id": 45, "raw_data": (".bin", f) }) -# Stored as: raw_data_kM3nP2qR.bin +# Stored as: raw_data_xY8zW3vN.bin ``` +### Remote URL Support + +Remote URLs are detected by protocol prefix and handled via fsspec: + +| Protocol | Example | Notes | +|----------|---------|-------| +| `s3://` | `s3://bucket/path/file.dat` | AWS S3, MinIO | +| `gs://` | `gs://bucket/path/file.dat` | Google Cloud Storage | +| `az://` | `az://container/path/file.dat` | Azure Blob Storage | +| `http://` | `http://server/path/file.dat` | HTTP (read-only source) | +| `https://` | `https://server/path/file.dat` | HTTPS (read-only source) | + +**Authentication**: Remote sources may require credentials. fsspec uses standard credential discovery (environment variables, config files, IAM roles). For cross-cloud copies, ensure credentials are configured for both source and destination. + +**Performance note**: For large remote-to-remote copies, data flows through the client. This is acceptable for most use cases but may be slow for very large datasets. Future optimizations could include server-side copy for same-provider transfers. + ### Insert Processing Steps 1. Validate input (file/folder exists, stream is readable) diff --git a/docs/src/design/tables/object.md b/docs/src/design/tables/object.md index 2efe0c0af..e2ed8bf25 100644 --- a/docs/src/design/tables/object.md +++ b/docs/src/design/tables/object.md @@ -89,7 +89,7 @@ Note: No `@store` suffix needed—storage is determined by pipeline configuratio ### Inserting Files -Insert a file by providing its path: +Insert a file by providing its local path: ```python Recording.insert1({ @@ -113,6 +113,37 @@ Recording.insert1({ }) ``` +### Inserting from Remote URLs + +Insert from cloud storage or HTTP sources—content is copied to managed storage: + +```python +# From S3 +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "s3://source-bucket/path/to/data.dat" +}) + +# From Google Cloud Storage (e.g., collaborator data) +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "neural_data": "gs://collaborator-bucket/shared/experiment.zarr" +}) + +# From HTTP/HTTPS +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "https://example.com/public/data.dat" +}) +``` + +Supported protocols: `s3://`, `gs://`, `az://`, `http://`, `https://` + +Remote sources may require credentials configured via environment variables or fsspec configuration files. + ### Inserting from Streams Insert from a file-like object with explicit extension: diff --git a/docs/src/manipulation/insert.md b/docs/src/manipulation/insert.md index 753e73b6c..2db4157d6 100644 --- a/docs/src/manipulation/insert.md +++ b/docs/src/manipulation/insert.md @@ -96,24 +96,38 @@ phase_two.Protocol.insert(protocols) ## Object attributes Tables with [`object`](../design/tables/object.md) type attributes can be inserted with -file paths, folder paths, or streams. The content is automatically copied to object -storage. +local file paths, folder paths, remote URLs, or streams. The content is automatically +copied to object storage. ```python -# Insert with file path +# Insert with local file path Recording.insert1({ "subject_id": 123, "session_id": 45, "raw_data": "/local/path/to/data.dat" }) -# Insert with folder path +# Insert with local folder path Recording.insert1({ "subject_id": 123, "session_id": 45, "raw_data": "/local/path/to/data_folder/" }) +# Insert from remote URL (S3, GCS, Azure, HTTP) +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "raw_data": "s3://source-bucket/path/to/data.dat" +}) + +# Insert remote Zarr store (e.g., from collaborator) +Recording.insert1({ + "subject_id": 123, + "session_id": 45, + "neural_data": "gs://collaborator-bucket/shared/experiment.zarr" +}) + # Insert from stream with explicit extension with open("/path/to/data.bin", "rb") as f: Recording.insert1({ @@ -123,6 +137,8 @@ with open("/path/to/data.bin", "rb") as f: }) ``` +Supported remote URL protocols: `s3://`, `gs://`, `az://`, `http://`, `https://` + ### Staged inserts For large objects like Zarr arrays, use `staged_insert1` to write directly to storage diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index c8b5c7b68..325364ea3 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -22,6 +22,55 @@ # Characters safe for use in filenames and URLs TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" +# Supported remote URL protocols for copy insert +REMOTE_PROTOCOLS = ("s3://", "gs://", "gcs://", "az://", "abfs://", "http://", "https://") + + +def is_remote_url(path: str) -> bool: + """ + Check if a path is a remote URL. + + Args: + path: Path string to check + + Returns: + True if path is a remote URL + """ + if not isinstance(path, str): + return False + return path.lower().startswith(REMOTE_PROTOCOLS) + + +def parse_remote_url(url: str) -> tuple[str, str]: + """ + Parse a remote URL into protocol and path. + + Args: + url: Remote URL (e.g., 's3://bucket/path/file.dat') + + Returns: + Tuple of (protocol, path) where protocol is fsspec-compatible + """ + url_lower = url.lower() + + # Map URL schemes to fsspec protocols + protocol_map = { + "s3://": "s3", + "gs://": "gcs", + "gcs://": "gcs", + "az://": "abfs", + "abfs://": "abfs", + "http://": "http", + "https://": "https", + } + + for prefix, protocol in protocol_map.items(): + if url_lower.startswith(prefix): + path = url[len(prefix) :] + return protocol, path + + raise errors.DataJointError(f"Unsupported remote URL protocol: {url}") + def generate_token(length: int = 8) -> str: """ @@ -494,6 +543,155 @@ def get_fsmap(self, remote_path: str | PurePosixPath) -> fsspec.FSMap: full_path = self._full_path(remote_path) return fsspec.FSMap(full_path, self.fs) + def copy_from_url(self, source_url: str, dest_path: str | PurePosixPath) -> int: + """ + Copy a file from a remote URL to managed storage. + + Args: + source_url: Remote URL (s3://, gs://, http://, etc.) + dest_path: Destination path in managed storage + + Returns: + Size of copied file in bytes + """ + protocol, source_path = parse_remote_url(source_url) + full_dest = self._full_path(dest_path) + + logger.debug(f"copy_from_url: {protocol}://{source_path} -> {self.protocol}:{full_dest}") + + # Get source filesystem + source_fs = fsspec.filesystem(protocol) + + # Check if source is a directory + if source_fs.isdir(source_path): + return self._copy_folder_from_url(source_fs, source_path, dest_path) + + # Copy single file + if self.protocol == "file": + # Download to local destination + Path(full_dest).parent.mkdir(parents=True, exist_ok=True) + source_fs.get_file(source_path, full_dest) + return Path(full_dest).stat().st_size + else: + # Remote-to-remote copy via streaming + with source_fs.open(source_path, "rb") as src: + content = src.read() + self.fs.pipe_file(full_dest, content) + return len(content) + + def _copy_folder_from_url( + self, source_fs: fsspec.AbstractFileSystem, source_path: str, dest_path: str | PurePosixPath + ) -> dict: + """ + Copy a folder from a remote URL to managed storage. + + Args: + source_fs: Source filesystem + source_path: Path in source filesystem + dest_path: Destination path in managed storage + + Returns: + Manifest dict with file list, total_size, and item_count + """ + full_dest = self._full_path(dest_path) + logger.debug(f"copy_folder_from_url: {source_path} -> {self.protocol}:{full_dest}") + + # Collect file info for manifest + files = [] + total_size = 0 + + # Walk source directory + for root, dirs, filenames in source_fs.walk(source_path): + for filename in filenames: + src_file = f"{root}/{filename}" if root != source_path else f"{source_path}/{filename}" + rel_path = src_file[len(source_path) :].lstrip("/") + file_size = source_fs.size(src_file) + files.append({"path": rel_path, "size": file_size}) + total_size += file_size + + # Copy file + dest_file = f"{full_dest}/{rel_path}" + if self.protocol == "file": + Path(dest_file).parent.mkdir(parents=True, exist_ok=True) + source_fs.get_file(src_file, dest_file) + else: + with source_fs.open(src_file, "rb") as src: + content = src.read() + self.fs.pipe_file(dest_file, content) + + # Build manifest + manifest = { + "files": files, + "total_size": total_size, + "item_count": len(files), + "created": datetime.now(timezone.utc).isoformat(), + } + + # Write manifest alongside folder + manifest_path = f"{dest_path}.manifest.json" + self.put_buffer(json.dumps(manifest, indent=2).encode(), manifest_path) + + return manifest + + def source_is_directory(self, source: str) -> bool: + """ + Check if a source path (local or remote URL) is a directory. + + Args: + source: Local path or remote URL + + Returns: + True if source is a directory + """ + if is_remote_url(source): + protocol, path = parse_remote_url(source) + source_fs = fsspec.filesystem(protocol) + return source_fs.isdir(path) + else: + return Path(source).is_dir() + + def source_exists(self, source: str) -> bool: + """ + Check if a source path (local or remote URL) exists. + + Args: + source: Local path or remote URL + + Returns: + True if source exists + """ + if is_remote_url(source): + protocol, path = parse_remote_url(source) + source_fs = fsspec.filesystem(protocol) + return source_fs.exists(path) + else: + return Path(source).exists() + + def get_source_size(self, source: str) -> int | None: + """ + Get the size of a source file (local or remote URL). + + Args: + source: Local path or remote URL + + Returns: + Size in bytes, or None if directory or cannot determine + """ + try: + if is_remote_url(source): + protocol, path = parse_remote_url(source) + source_fs = fsspec.filesystem(protocol) + if source_fs.isdir(path): + return None + return source_fs.size(path) + else: + p = Path(source) + if p.is_dir(): + return None + return p.stat().st_size + except Exception: + return None + STORE_METADATA_FILENAME = "datajoint_store.json" diff --git a/tests/test_object.py b/tests/test_object.py index 8b8a34056..c2fd18cf6 100644 --- a/tests/test_object.py +++ b/tests/test_object.py @@ -759,3 +759,94 @@ def test_staged_insert_missing_pk_raises(self, schema_obj, mock_object_storage): with table.staged_insert1 as staged: # Don't set primary key staged.store("data_file", ".dat") + + +class TestRemoteURLSupport: + """Tests for remote URL detection and parsing.""" + + def test_is_remote_url_s3(self): + """Test S3 URL detection.""" + from datajoint.storage import is_remote_url + + assert is_remote_url("s3://bucket/path/file.dat") is True + assert is_remote_url("S3://bucket/path/file.dat") is True + + def test_is_remote_url_gcs(self): + """Test GCS URL detection.""" + from datajoint.storage import is_remote_url + + assert is_remote_url("gs://bucket/path/file.dat") is True + assert is_remote_url("gcs://bucket/path/file.dat") is True + + def test_is_remote_url_azure(self): + """Test Azure URL detection.""" + from datajoint.storage import is_remote_url + + assert is_remote_url("az://container/path/file.dat") is True + assert is_remote_url("abfs://container/path/file.dat") is True + + def test_is_remote_url_http(self): + """Test HTTP/HTTPS URL detection.""" + from datajoint.storage import is_remote_url + + assert is_remote_url("http://example.com/path/file.dat") is True + assert is_remote_url("https://example.com/path/file.dat") is True + + def test_is_remote_url_local_path(self): + """Test local paths are not detected as remote.""" + from datajoint.storage import is_remote_url + + assert is_remote_url("/local/path/file.dat") is False + assert is_remote_url("relative/path/file.dat") is False + assert is_remote_url("C:\\Windows\\path\\file.dat") is False + + def test_is_remote_url_non_string(self): + """Test non-string inputs return False.""" + from datajoint.storage import is_remote_url + + assert is_remote_url(None) is False + assert is_remote_url(123) is False + assert is_remote_url(Path("/local/path")) is False + + def test_parse_remote_url_s3(self): + """Test S3 URL parsing.""" + from datajoint.storage import parse_remote_url + + protocol, path = parse_remote_url("s3://bucket/path/file.dat") + assert protocol == "s3" + assert path == "bucket/path/file.dat" + + def test_parse_remote_url_gcs(self): + """Test GCS URL parsing.""" + from datajoint.storage import parse_remote_url + + protocol, path = parse_remote_url("gs://bucket/path/file.dat") + assert protocol == "gcs" + assert path == "bucket/path/file.dat" + + protocol, path = parse_remote_url("gcs://bucket/path/file.dat") + assert protocol == "gcs" + assert path == "bucket/path/file.dat" + + def test_parse_remote_url_azure(self): + """Test Azure URL parsing.""" + from datajoint.storage import parse_remote_url + + protocol, path = parse_remote_url("az://container/path/file.dat") + assert protocol == "abfs" + assert path == "container/path/file.dat" + + def test_parse_remote_url_http(self): + """Test HTTP URL parsing.""" + from datajoint.storage import parse_remote_url + + protocol, path = parse_remote_url("https://example.com/path/file.dat") + assert protocol == "https" + assert path == "example.com/path/file.dat" + + def test_parse_remote_url_unsupported(self): + """Test unsupported protocol raises error.""" + from datajoint.storage import parse_remote_url + + with pytest.raises(dj.DataJointError, match="Unsupported remote URL"): + parse_remote_url("ftp://server/path/file.dat") From 4bdc8827520cc6b761c8c7b11cf854e7398aa130 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 18:42:19 +0000 Subject: [PATCH 086/219] Remove redundant self.spec attribute from ExternalTable Access spec via self.storage.spec instead of storing it as a separate attribute. StorageBackend already stores the spec internally. --- src/datajoint/external.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/datajoint/external.py b/src/datajoint/external.py index 6e00a67b9..b3cbc17a8 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -38,7 +38,6 @@ class ExternalTable(Table): def __init__(self, connection, store, database): self.store = store - self.spec = config.get_store_spec(store) self.database = database self._connection = connection self._heading = Heading( @@ -53,7 +52,7 @@ def __init__(self, connection, store, database): if not self.is_declared: self.declare() # Initialize storage backend (validates configuration) - self.storage = StorageBackend(self.spec) + self.storage = StorageBackend(config.get_store_spec(store)) @property def definition(self): @@ -84,28 +83,29 @@ def s3(self): from . import s3 if not hasattr(self, "_s3_legacy") or self._s3_legacy is None: - self._s3_legacy = s3.Folder(**self.spec) + self._s3_legacy = s3.Folder(**self.storage.spec) return self._s3_legacy # - low-level operations - private def _make_external_filepath(self, relative_filepath): """resolve the complete external path based on the relative path""" + spec = self.storage.spec # Strip root for S3 paths - if self.spec["protocol"] == "s3": - posix_path = PurePosixPath(PureWindowsPath(self.spec["location"])) + if spec["protocol"] == "s3": + posix_path = PurePosixPath(PureWindowsPath(spec["location"])) location_path = ( Path(*posix_path.parts[1:]) - if len(self.spec["location"]) > 0 and any(case in posix_path.parts[0] for case in ("\\", ":")) + if len(spec["location"]) > 0 and any(case in posix_path.parts[0] for case in ("\\", ":")) else Path(posix_path) ) return PurePosixPath(location_path, relative_filepath) # Preserve root for local filesystem - elif self.spec["protocol"] == "file": - return PurePosixPath(Path(self.spec["location"]), relative_filepath) + elif spec["protocol"] == "file": + return PurePosixPath(Path(spec["location"]), relative_filepath) else: # For other protocols (gcs, azure, etc.), treat like S3 - location = self.spec.get("location", "") + location = spec.get("location", "") return PurePosixPath(location, relative_filepath) if location else PurePosixPath(relative_filepath) def _make_uuid_path(self, uuid, suffix=""): @@ -113,7 +113,7 @@ def _make_uuid_path(self, uuid, suffix=""): return self._make_external_filepath( PurePosixPath( self.database, - "/".join(subfold(uuid.hex, self.spec["subfolding"])), + "/".join(subfold(uuid.hex, self.storage.spec["subfolding"])), uuid.hex, ).with_suffix(suffix) ) @@ -235,9 +235,11 @@ def upload_filepath(self, local_filepath): """ local_filepath = Path(local_filepath) try: - relative_filepath = str(local_filepath.relative_to(self.spec["stage"]).as_posix()) + relative_filepath = str(local_filepath.relative_to(self.storage.spec["stage"]).as_posix()) except ValueError: - raise DataJointError("The path {path} is not in stage {stage}".format(path=local_filepath.parent, **self.spec)) + raise DataJointError( + f"The path {local_filepath.parent} is not in stage {self.storage.spec['stage']}" + ) uuid = uuid_from_buffer(init_string=relative_filepath) # hash relative path, not contents contents_hash = uuid_from_file(local_filepath) @@ -285,7 +287,7 @@ def _need_checksum(local_filepath, expected_size): "filepath", "contents_hash", "size" ) external_path = self._make_external_filepath(relative_filepath) - local_filepath = Path(self.spec["stage"]).absolute() / relative_filepath + local_filepath = Path(self.storage.spec["stage"]).absolute() / relative_filepath file_exists = Path(local_filepath).is_file() and ( not _need_checksum(local_filepath, size) or uuid_from_file(local_filepath) == contents_hash From cc96f03660452b07f6685be3b88977cd53c65a52 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 18:45:28 +0000 Subject: [PATCH 087/219] Fix ruff-format: single line error message in upload_filepath --- src/datajoint/external.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/datajoint/external.py b/src/datajoint/external.py index b3cbc17a8..06e76af37 100644 --- a/src/datajoint/external.py +++ b/src/datajoint/external.py @@ -237,9 +237,7 @@ def upload_filepath(self, local_filepath): try: relative_filepath = str(local_filepath.relative_to(self.storage.spec["stage"]).as_posix()) except ValueError: - raise DataJointError( - f"The path {local_filepath.parent} is not in stage {self.storage.spec['stage']}" - ) + raise DataJointError(f"The path {local_filepath.parent} is not in stage {self.storage.spec['stage']}") uuid = uuid_from_buffer(init_string=relative_filepath) # hash relative path, not contents contents_hash = uuid_from_file(local_filepath) From 45a94e1be15b0ecdf8cafb0d6840aa021b25b807 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Dec 2025 22:12:08 +0000 Subject: [PATCH 088/219] refactor: rename TYPE_ALIASES to SQL_TYPE_ALIASES --- src/datajoint/declare.py | 8 ++++---- tests/test_type_aliases.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index e21193e50..f23f74a26 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -17,7 +17,7 @@ UUID_DATA_TYPE = "binary(16)" # Type aliases for numeric types -TYPE_ALIASES = { +SQL_TYPE_ALIASES = { "FLOAT32": "float", "FLOAT64": "double", "INT64": "bigint", @@ -78,7 +78,7 @@ "EXTERNAL_BLOB", "FILEPATH", "ADAPTED", -} | set(TYPE_ALIASES) +} | set(SQL_TYPE_ALIASES) NATIVE_TYPES = set(TYPE_PATTERN) - SPECIAL_TYPES EXTERNAL_TYPES = { "EXTERNAL_ATTACH", @@ -487,8 +487,8 @@ def substitute_special_type(match, category, foreign_key_sql, context): if category in SPECIAL_TYPES: # recursive redefinition from user-defined datatypes. substitute_special_type(match, category, foreign_key_sql, context) - elif category in TYPE_ALIASES: - match["type"] = TYPE_ALIASES[category] + elif category in SQL_TYPE_ALIASES: + match["type"] = SQL_TYPE_ALIASES[category] else: assert False, "Unknown special type" diff --git a/tests/test_type_aliases.py b/tests/test_type_aliases.py index 019b69498..1cf227ac8 100644 --- a/tests/test_type_aliases.py +++ b/tests/test_type_aliases.py @@ -4,7 +4,7 @@ import pytest -from datajoint.declare import TYPE_ALIASES, SPECIAL_TYPES, match_type +from datajoint.declare import SQL_TYPE_ALIASES, SPECIAL_TYPES, match_type from .schema_type_aliases import TypeAliasTable, TypeAliasPrimaryKey, TypeAliasNullable @@ -33,7 +33,7 @@ def test_type_alias_pattern_matching(self, alias, expected_category): category = match_type(alias) assert category == expected_category assert category in SPECIAL_TYPES - assert category in TYPE_ALIASES + assert category in SQL_TYPE_ALIASES @pytest.mark.parametrize( "alias,expected_mysql_type", @@ -54,7 +54,7 @@ def test_type_alias_pattern_matching(self, alias, expected_category): def test_type_alias_mysql_mapping(self, alias, expected_mysql_type): """Test that type aliases map to correct MySQL types.""" category = match_type(alias) - mysql_type = TYPE_ALIASES[category] + mysql_type = SQL_TYPE_ALIASES[category] assert mysql_type == expected_mysql_type @pytest.mark.parametrize( From d5439cf0476bc990b2d868e71114e17dd8f172f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 21:23:52 +0000 Subject: [PATCH 089/219] Address reviewer feedback on object type spec - Add Configuration Immutability section warning about changing settings - Clarify database_name is for multi-database DBMS platforms - Implement =OBJ[.ext]= display format in preview.py for query results - Add objects property to Heading class - Add ObjectRef.to_dict() method for raw metadata access - Fix conflicting text about staged insert hashing - Document explicit hash kwarg with design principles - Rename file_storage to object_storage utility - Document grace period for orphan cleanup race condition --- docs/src/design/tables/object-type-spec.md | 80 ++++++++++++++---- src/datajoint/heading.py | 4 + src/datajoint/objectref.py | 21 +++++ src/datajoint/preview.py | 96 +++++++++++++++++++++- 4 files changed, 180 insertions(+), 21 deletions(-) diff --git a/docs/src/design/tables/object-type-spec.md b/docs/src/design/tables/object-type-spec.md index dea83c5f4..bc270b0ad 100644 --- a/docs/src/design/tables/object-type-spec.md +++ b/docs/src/design/tables/object-type-spec.md @@ -134,6 +134,20 @@ For local filesystem storage: | `object_storage.access_key` | string | For cloud | Access key (can use secrets file) | | `object_storage.secret_key` | string | For cloud | Secret key (can use secrets file) | +### Configuration Immutability + +**CRITICAL**: Once a project has been instantiated (i.e., `datajoint_store.json` has been created and the first object stored), the following settings MUST NOT be changed: + +- `object_storage.project_name` +- `object_storage.protocol` +- `object_storage.bucket` +- `object_storage.location` +- `object_storage.partition_pattern` + +Changing these settings after objects have been stored will result in **broken references**—existing paths stored in the database will no longer resolve to valid storage locations. + +DataJoint validates `project_name` against `datajoint_store.json` on connect, but administrators must ensure other settings remain consistent across all clients for the lifetime of the project. + ### Environment Variables Settings can be overridden via environment variables: @@ -210,9 +224,16 @@ s3://bucket/my_project/datajoint_store.json | `format_version` | string | Yes | Store format version for compatibility | | `datajoint_version` | string | Yes | DataJoint version that created the store | | `database_host` | string | No | Database server hostname (for bidirectional mapping) | -| `database_name` | string | No | Database name (for bidirectional mapping) | +| `database_name` | string | No | Database name on the server (for bidirectional mapping) | -The optional `database_host` and `database_name` fields enable bidirectional mapping between object stores and databases. This is informational only - not enforced at runtime. Administrators can alternatively ensure unique `project_name` values across their namespace, and managed platforms may handle this mapping externally. +The `database_name` field exists for DBMS platforms that support multiple databases on a single server (e.g., PostgreSQL, MySQL). The object storage configuration is **shared across all schemas comprising the pipeline**—it's a pipeline-level setting, not a per-schema setting. + +The optional `database_host` and `database_name` fields enable bidirectional mapping between object stores and databases: + +- **Forward**: Client settings → object store location +- **Reverse**: Object store metadata → originating database + +This is informational only—not enforced at runtime. Administrators can alternatively ensure unique `project_name` values across their namespace, and managed platforms may handle this mapping externally. ### Store Initialization @@ -362,19 +383,28 @@ For large hierarchical data like Zarr stores, computing certain metadata can be By default, **no content hash is computed** to avoid performance overhead for large objects. Storage backend integrity is trusted. -**Optional hashing** can be requested per-insert: +**Explicit hash control** via insert kwarg: ```python # Default - no hash (fast) Recording.insert1({..., "raw_data": "/path/to/large.dat"}) -# Request hash computation +# Explicit hash request - user specifies algorithm Recording.insert1({..., "raw_data": "/path/to/important.dat"}, hash="sha256") + +# Other supported algorithms +Recording.insert1({..., "raw_data": "/path/to/data.bin"}, hash="md5") +Recording.insert1({..., "raw_data": "/path/to/large.bin"}, hash="xxhash") # xxh3, faster for large files ``` -Supported hash algorithms: `sha256`, `md5`, `xxhash` (xxh3, faster for large files) +**Design principles:** + +- **Explicit over implicit**: No automatic hashing based on file size or other heuristics +- **User controls the tradeoff**: User decides when integrity verification is worth the performance cost +- **Files only**: Hash applies to files, not folders (folders use manifests for integrity) +- **Staged inserts**: Hash is always `null` regardless of kwarg—data flows directly to storage without a local copy to hash -**Staged inserts never compute hashes** - data is written directly to storage without a local copy to hash. +Supported hash algorithms: `sha256`, `md5`, `xxhash` (xxh3, faster for large files) ### Folder Manifests @@ -654,7 +684,7 @@ Remote URLs are detected by protocol prefix and handled via fsspec: 2. Generate deterministic storage path with random token 3. **Copy content to storage backend** via `fsspec` 4. **If copy fails: abort insert** (no database operation attempted) -5. Compute content hash (SHA-256) +5. Compute content hash if requested (optional, default: no hash) 6. Build JSON metadata structure 7. Execute database INSERT @@ -758,7 +788,7 @@ class StagedInsert: │ 4. User assigns object references to staged.rec │ ├─────────────────────────────────────────────────────────┤ │ 5. On context exit (success): │ -│ - Compute metadata (size, hash, item_count) │ +│ - Build metadata (size/item_count optional, no hash) │ │ - Execute database INSERT │ ├─────────────────────────────────────────────────────────┤ │ 6. On context exit (exception): │ @@ -839,7 +869,7 @@ Since storage backends don't support distributed transactions with MySQL, DataJo │ 2. Copy file/folder to storage backend │ │ └─ On failure: raise error, INSERT not attempted │ ├─────────────────────────────────────────────────────────┤ -│ 3. Compute hash and build JSON metadata │ +│ 3. Compute hash (if requested) and build JSON metadata │ ├─────────────────────────────────────────────────────────┤ │ 4. Execute database INSERT │ │ └─ On failure: orphaned file remains (acceptable) │ @@ -871,19 +901,35 @@ Orphaned files (files in storage without corresponding database records) may acc ### Orphan Cleanup Procedure -Orphan cleanup is a **separate maintenance operation** that must be performed during maintenance windows to avoid race conditions with concurrent inserts. +Orphan cleanup is a **separate maintenance operation** provided via the `schema.object_storage` utility object. ```python -# Maintenance utility methods -schema.file_storage.find_orphaned() # List files not referenced in DB -schema.file_storage.cleanup_orphaned() # Delete orphaned files +# Maintenance utility methods (not a hidden table) +schema.object_storage.find_orphaned(grace_period_minutes=30) # List orphaned files +schema.object_storage.cleanup_orphaned(dry_run=True) # Delete orphaned files +schema.object_storage.verify_integrity() # Check all objects exist +schema.object_storage.stats() # Storage usage statistics ``` +**Note**: `schema.object_storage` is a utility object, not a hidden table. Unlike `attach@store` which uses `~external_*` tables, the `object` type stores all metadata inline in JSON columns and has no hidden tables. + +**Grace period for in-flight inserts:** + +While random tokens prevent filename collisions, there's a race condition with in-flight inserts: + +1. Insert starts: file copied to storage with token `Ax7bQ2kM` +2. Orphan cleanup runs: lists storage, queries DB for references +3. File `Ax7bQ2kM` not yet in DB (INSERT not committed) +4. Cleanup identifies it as orphan and deletes it +5. Insert commits: DB now references deleted file! + +**Solution**: The `grace_period_minutes` parameter (default: 30) excludes files created within that window, assuming they are in-flight inserts. + **Important considerations:** -- Should be run during low-activity periods -- Uses transactions or locking to avoid race conditions with concurrent inserts -- Files recently uploaded (within a grace period) are excluded to handle in-flight inserts -- Provides dry-run mode to preview deletions before execution +- Grace period handles race conditions—cleanup is safe to run anytime +- Running during low-activity periods reduces in-flight operations to reason about +- `dry_run=True` previews deletions before execution +- Compares storage contents against JSON metadata in table columns ## Fetch Behavior diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index 58f46cc0d..1ef1d7a08 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -135,6 +135,10 @@ def secondary_attributes(self): def blobs(self): return [k for k, v in self.attributes.items() if v.is_blob] + @property + def objects(self): + return [k for k, v in self.attributes.items() if v.is_object] + @property def non_blobs(self): return [ diff --git a/src/datajoint/objectref.py b/src/datajoint/objectref.py index 32f7b1669..9c1aca2fa 100644 --- a/src/datajoint/objectref.py +++ b/src/datajoint/objectref.py @@ -111,6 +111,27 @@ def to_json(self) -> dict: data["item_count"] = self.item_count return data + def to_dict(self) -> dict: + """ + Return the raw JSON metadata as a dictionary. + + This is useful for inspecting the stored metadata without triggering + any storage backend operations. The returned dict matches the JSON + structure stored in the database. + + Returns: + Dict containing the object metadata: + - path: Storage path + - size: File/folder size in bytes (or None) + - hash: Content hash (or None) + - ext: File extension (or None) + - is_dir: True if folder + - timestamp: Upload timestamp + - mime_type: MIME type (files only, optional) + - item_count: Number of files (folders only, optional) + """ + return self.to_json() + def _ensure_backend(self): """Ensure storage backend is available for I/O operations.""" if self._backend is None: diff --git a/src/datajoint/preview.py b/src/datajoint/preview.py index 4fd0d1fe5..9a05b054f 100644 --- a/src/datajoint/preview.py +++ b/src/datajoint/preview.py @@ -1,11 +1,45 @@ """methods for generating previews of query expression results in python command line and Jupyter""" +import json + from .settings import config +def _format_object_display(json_data): + """Format object metadata for display in query results.""" + if json_data is None: + return "=OBJ[null]=" + if isinstance(json_data, str): + try: + json_data = json.loads(json_data) + except (json.JSONDecodeError, TypeError): + return "=OBJ=?" + ext = json_data.get("ext") + is_dir = json_data.get("is_dir", False) + if ext: + return f"=OBJ[{ext}]=" + elif is_dir: + return "=OBJ[folder]=" + else: + return "=OBJ[file]=" + + +def _get_display_value(tup, field, object_fields, object_data): + """Get display value for a field, handling objects specially.""" + if field in tup.dtype.names: + return tup[field] + elif field in object_fields and object_data is not None: + # Find the matching tuple in object_data by index + idx = list(tup.dtype.names).index(list(tup.dtype.names)[0]) # placeholder + return _format_object_display(object_data.get(field)) + else: + return "=BLOB=" + + def preview(query_expression, limit, width): heading = query_expression.heading rel = query_expression.proj(*heading.non_blobs) + object_fields = heading.objects if limit is None: limit = config["display.limit"] if width is None: @@ -13,21 +47,52 @@ def preview(query_expression, limit, width): tuples = rel.fetch(limit=limit + 1, format="array") has_more = len(tuples) > limit tuples = tuples[:limit] + + # Fetch object field JSON data for display (raw JSON, not ObjectRef) + object_data_list = [] + if object_fields: + # Fetch primary key and object fields as dicts + obj_rel = query_expression.proj(*object_fields) + obj_tuples = obj_rel.fetch(limit=limit, format="array") + for obj_tup in obj_tuples: + obj_dict = {} + for field in object_fields: + if field in obj_tup.dtype.names: + obj_dict[field] = obj_tup[field] + object_data_list.append(obj_dict) + columns = heading.names + + def get_placeholder(f): + if f in object_fields: + return "=OBJ[.xxx]=" + return "=BLOB=" + widths = { f: min( - max([len(f)] + [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else [len("=BLOB=")]) + 4, + max([len(f)] + [len(str(e)) for e in tuples[f]] if f in tuples.dtype.names else [len(get_placeholder(f))]) + 4, width, ) for f in columns } templates = {f: "%%-%d.%ds" % (widths[f], widths[f]) for f in columns} + + def get_display_value(tup, f, idx): + if f in tup.dtype.names: + return tup[f] + elif f in object_fields and idx < len(object_data_list): + return _format_object_display(object_data_list[idx].get(f)) + else: + return "=BLOB=" + return ( " ".join([templates[f] % ("*" + f if f in rel.primary_key else f) for f in columns]) + "\n" + " ".join(["+" + "-" * (widths[column] - 2) + "+" for column in columns]) + "\n" - + "\n".join(" ".join(templates[f] % (tup[f] if f in tup.dtype.names else "=BLOB=") for f in columns) for tup in tuples) + + "\n".join( + " ".join(templates[f] % get_display_value(tup, f, idx) for f in columns) for idx, tup in enumerate(tuples) + ) + ("\n ...\n" if has_more else "\n") + (" (Total: %d)\n" % len(rel) if config["display.show_tuple_count"] else "") ) @@ -36,11 +101,32 @@ def preview(query_expression, limit, width): def repr_html(query_expression): heading = query_expression.heading rel = query_expression.proj(*heading.non_blobs) + object_fields = heading.objects info = heading.table_status tuples = rel.fetch(limit=config["display.limit"] + 1, format="array") has_more = len(tuples) > config["display.limit"] tuples = tuples[0 : config["display.limit"]] + # Fetch object field JSON data for display (raw JSON, not ObjectRef) + object_data_list = [] + if object_fields: + obj_rel = query_expression.proj(*object_fields) + obj_tuples = obj_rel.fetch(limit=config["display.limit"], format="array") + for obj_tup in obj_tuples: + obj_dict = {} + for field in object_fields: + if field in obj_tup.dtype.names: + obj_dict[field] = obj_tup[field] + object_data_list.append(obj_dict) + + def get_html_display_value(tup, name, idx): + if name in tup.dtype.names: + return tup[name] + elif name in object_fields and idx < len(object_data_list): + return _format_object_display(object_data_list[idx].get(name)) + else: + return "=BLOB=" + css = """ """ head_template = """
From 3f5b237e6628643ffba4c5a64c89ae862d57af79 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:12 -0700 Subject: [PATCH 198/219] fix: return 0 from delete() when user cancels operation (#1155) (#1316) When a user answers "no" to "Commit deletes?", the transaction is rolled back but delete() still returned the count of rows that would have been deleted. This was unintuitive - if nothing was deleted, the return value should be 0. Now delete() returns 0 when: - User cancels at the prompt - Nothing to delete (already worked correctly) Fixes #1155 Co-authored-by: Claude Opus 4.5 --- src/datajoint/table.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 7543c385e..9463e52f3 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -977,6 +977,7 @@ def cascade(table): self.connection.cancel_transaction() if prompt: logger.warning("Delete cancelled") + delete_count = 0 # Reset count when delete is cancelled return delete_count def drop_quick(self): From 5f2847c9f9d9096729ae7286fae74fbe62d363b5 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:18 -0700 Subject: [PATCH 199/219] fix: handle TypeError in lookup_class_name for Diagram (#1072) (#1317) When inspect.getmembers() encounters modules with objects that have non-standard __bases__ attributes (like _ClassNamespace from typing internals), it raises TypeError. This caused dj.Diagram(schema) to fail intermittently depending on what modules were imported. Now catches TypeError in addition to ImportError, allowing the search to continue by skipping problematic modules. Fixes #1072 Co-authored-by: Claude Opus 4.5 --- src/datajoint/table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 9463e52f3..346db97f9 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -1307,8 +1307,8 @@ def lookup_class_name(name, context, depth=3): depth=node["depth"] - 1, ) ) - except ImportError: - pass # could not import, so do not attempt + except (ImportError, TypeError): + pass # could not inspect module members, skip return None From d38a6702fc3c221e09839f14e5e20a4fee2be817 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:24 -0700 Subject: [PATCH 200/219] refactor: remove text from core types, keep as native passthrough (#1318) `text` is no longer a core DataJoint type. It remains available as a native SQL passthrough type (with portability warning). Rationale: - Core types should encourage structured, bounded data - varchar(n) covers most legitimate text needs with explicit bounds - json handles structured text better - is better for large/unbounded text (files, sequences, docs) - text behavior varies across databases, hurting portability Changes: - Remove `text` from CORE_TYPES in declare.py - Update NATIVE_TEXT pattern to match plain `text` (in addition to tinytext, mediumtext, longtext) - Update archive docs to note text is native-only Users who need unlimited text can: - Use varchar(n) with generous limit - Use json for structured content - Use for large text files - Use native text types with portability warning Co-authored-by: Claude Opus 4.5 --- docs/src/archive/design/tables/attributes.md | 4 +++- docs/src/archive/design/tables/storage-types-spec.md | 4 +++- src/datajoint/declare.py | 4 +--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/src/archive/design/tables/attributes.md b/docs/src/archive/design/tables/attributes.md index 39a80ff67..3753621d5 100644 --- a/docs/src/archive/design/tables/attributes.md +++ b/docs/src/archive/design/tables/attributes.md @@ -34,10 +34,12 @@ Use these portable, scientist-friendly types for cross-database compatibility. - `char(n)`: fixed-length string of exactly *n* characters. - `varchar(n)`: variable-length string up to *n* characters. -- `text`: unlimited-length text for long-form content (notes, descriptions, abstracts). - `enum(...)`: one of several enumerated values, e.g., `enum("low", "medium", "high")`. Do not use enums in primary keys due to difficulty changing definitions. +> **Note:** For unlimited text, use `varchar` with a generous limit, `json` for structured content, +> or `` for large text files. Native SQL `text` types are supported but not portable. + **Encoding policy:** All strings use UTF-8 encoding (`utf8mb4` in MySQL, `UTF8` in PostgreSQL). Character encoding and collation are database-level configuration, not part of type definitions. Comparisons are case-sensitive by default. diff --git a/docs/src/archive/design/tables/storage-types-spec.md b/docs/src/archive/design/tables/storage-types-spec.md index f7aead7de..7157d4d42 100644 --- a/docs/src/archive/design/tables/storage-types-spec.md +++ b/docs/src/archive/design/tables/storage-types-spec.md @@ -75,7 +75,9 @@ MySQL and PostgreSQL backends. Users should prefer these over native database ty |-----------|-------------|-------|------------| | `char(n)` | Fixed-length | `CHAR(n)` | `CHAR(n)` | | `varchar(n)` | Variable-length | `VARCHAR(n)` | `VARCHAR(n)` | -| `text` | Unlimited text | `TEXT` | `TEXT` | + +> **Note:** Native SQL `text` types (`text`, `tinytext`, `mediumtext`, `longtext`) are supported +> but not portable. Prefer `varchar(n)`, `json`, or `` for portable schemas. **Encoding:** All strings use UTF-8 (`utf8mb4` in MySQL, `UTF8` in PostgreSQL). See [Encoding and Collation Policy](#encoding-and-collation-policy) for details. diff --git a/src/datajoint/declare.py b/src/datajoint/declare.py index d8479b124..d86e90ed9 100644 --- a/src/datajoint/declare.py +++ b/src/datajoint/declare.py @@ -45,8 +45,6 @@ # String types (with parameters) "char": (r"char\s*\(\d+\)$", None), "varchar": (r"varchar\s*\(\d+\)$", None), - # Unlimited text - "text": (r"text$", None), # Enumeration "enum": (r"enum\s*\(.+\)$", None), # Fixed-point decimal @@ -78,7 +76,7 @@ STRING=r"(var)?char\s*\(.+\)$", # Catches char/varchar not matched by core types TEMPORAL=r"(time|timestamp|year)(\s*\(.+\))?$", # time, timestamp, year (not date/datetime) NATIVE_BLOB=r"(tiny|small|medium|long)blob$", # Specific blob variants - NATIVE_TEXT=r"(tiny|small|medium|long)text$", # Text variants (use plain 'text' instead) + NATIVE_TEXT=r"(tiny|small|medium|long)?text$", # Native text types (not portable) # Codecs use angle brackets CODEC=r"<.+>$", ).items() From 1673be8925b354b4f89f8e701eca69882a8632bd Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:31 -0700 Subject: [PATCH 201/219] fix: translate MySQL error 3730 to IntegrityError (#1032) (#1319) When dropping a table/schema referenced by foreign key constraints, MySQL returns error 3730. This was passing through as a raw pymysql OperationalError, making it difficult for users to catch and handle. Now translates to datajoint.errors.IntegrityError, consistent with other foreign key constraint errors (1217, 1451, 1452). Before: pymysql.err.OperationalError: (3730, "Cannot drop table...") After: datajoint.errors.IntegrityError: Cannot drop table '#table' referenced by a foreign key constraint... Fixes #1032 Co-authored-by: Claude Opus 4.5 --- src/datajoint/connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/datajoint/connection.py b/src/datajoint/connection.py index 394952886..0736eba11 100644 --- a/src/datajoint/connection.py +++ b/src/datajoint/connection.py @@ -66,7 +66,11 @@ def translate_query_error(client_error: Exception, query: str) -> Exception: # Integrity errors case 1062: return errors.DuplicateError(*args) - case 1217 | 1451 | 1452: + case 1217 | 1451 | 1452 | 3730: + # 1217: Cannot delete parent row (FK constraint) + # 1451: Cannot delete/update parent row (FK constraint) + # 1452: Cannot add/update child row (FK constraint) + # 3730: Cannot drop table referenced by FK constraint return errors.IntegrityError(*args) # Syntax errors From 1270d901b5ffb4becf51070441f634d2ba2c8779 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:36 -0700 Subject: [PATCH 202/219] feat: add context manager support to Connection class (#1320) Add __enter__ and __exit__ methods to Connection for use with Python's `with` statement. This enables automatic connection cleanup, particularly useful for serverless environments (AWS Lambda, Cloud Functions). Usage: with dj.Connection(host, user, password) as conn: schema = dj.schema('my_schema', connection=conn) # perform operations # connection automatically closed Closes #1081 Co-authored-by: Claude Opus 4.5 --- src/datajoint/connection.py | 39 ++++++++++++++++++++++++++++ tests/integration/test_connection.py | 30 +++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/datajoint/connection.py b/src/datajoint/connection.py index 0736eba11..57301c2f3 100644 --- a/src/datajoint/connection.py +++ b/src/datajoint/connection.py @@ -287,6 +287,45 @@ def close(self) -> None: """Close the database connection.""" self._conn.close() + def __enter__(self) -> "Connection": + """ + Enter context manager. + + Returns + ------- + Connection + This connection object. + + Examples + -------- + >>> with dj.Connection(host, user, password) as conn: + ... schema = dj.schema('my_schema', connection=conn) + ... # perform operations + ... # connection automatically closed + """ + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> bool: + """ + Exit context manager and close connection. + + Parameters + ---------- + exc_type : type or None + Exception type if an exception was raised. + exc_val : Exception or None + Exception instance if an exception was raised. + exc_tb : traceback or None + Traceback if an exception was raised. + + Returns + ------- + bool + False to propagate exceptions. + """ + self.close() + return False + def register(self, schema) -> None: """ Register a schema with this connection. diff --git a/tests/integration/test_connection.py b/tests/integration/test_connection.py index 8a30d4a46..ff3940587 100644 --- a/tests/integration/test_connection.py +++ b/tests/integration/test_connection.py @@ -46,6 +46,36 @@ def test_dj_connection_class(connection_test): assert connection_test.is_connected +def test_connection_context_manager(db_creds_test): + """ + Connection should support context manager protocol for automatic cleanup. + """ + # Test basic context manager usage + with dj.Connection(**db_creds_test) as conn: + assert conn.is_connected + # Verify we can use the connection + result = conn.query("SELECT 1").fetchone() + assert result[0] == 1 + + # Connection should be closed after exiting context + assert not conn.is_connected + + +def test_connection_context_manager_exception(db_creds_test): + """ + Connection should close even when exception is raised inside context. + """ + conn = None + with pytest.raises(ValueError): + with dj.Connection(**db_creds_test) as conn: + assert conn.is_connected + raise ValueError("Test exception") + + # Connection should still be closed after exception + assert conn is not None + assert not conn.is_connected + + def test_persistent_dj_conn(db_creds_root): """ conn() method should provide persistent connection across calls. From 299ac0df4f6f3d0c0224b601ee7fe10a291ad826 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:47 -0700 Subject: [PATCH 203/219] perf: implement lazy imports for heavy dependencies (#1321) * perf: implement lazy imports for heavy dependencies Defer loading of heavy dependencies (networkx, matplotlib, click, pymysql) until their associated features are accessed: - dj.Diagram, dj.Di, dj.ERD -> loads diagram.py (networkx, matplotlib) - dj.kill -> loads admin.py (pymysql via connection) - dj.cli -> loads cli.py (click) This reduces `import datajoint` time significantly, especially on macOS where import overhead is higher. Core functionality (Schema, Table, Connection, etc.) remains immediately available. Closes #1220 Co-Authored-By: Claude Opus 4.5 * fix: cache lazy imports correctly and expose diagram module - Cache lazy imports in globals() to override the submodule that importlib automatically sets on the parent module - Add dj.diagram to lazy modules (returns module for diagram_active access) - Add tests for cli callable and diagram module access Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- src/datajoint/__init__.py | 43 ++++++++++-- tests/unit/test_lazy_imports.py | 121 ++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 tests/unit/test_lazy_imports.py diff --git a/src/datajoint/__init__.py b/src/datajoint/__init__.py index e4077816c..ae8d308d2 100644 --- a/src/datajoint/__init__.py +++ b/src/datajoint/__init__.py @@ -60,18 +60,18 @@ "ValidationResult", ] +# ============================================================================= +# Eager imports — core functionality needed immediately +# ============================================================================= from . import errors from . import migrate -from .admin import kill from .codecs import ( Codec, get_codec, list_codecs, ) from .blob import MatCell, MatStruct -from .cli import cli from .connection import Connection, conn -from .diagram import Diagram from .errors import DataJointError from .expression import AndList, Not, Top, U from .hash import key_hash @@ -83,5 +83,38 @@ from .user_tables import Computed, Imported, Lookup, Manual, Part from .version import __version__ -ERD = Di = Diagram # Aliases for Diagram -schema = Schema # Aliases for Schema +schema = Schema # Alias for Schema + +# ============================================================================= +# Lazy imports — heavy dependencies loaded on first access +# ============================================================================= +# These modules import heavy dependencies (networkx, matplotlib, click, pymysql) +# that slow down `import datajoint`. They are loaded on demand. + +_lazy_modules = { + # Diagram imports networkx and matplotlib + "Diagram": (".diagram", "Diagram"), + "Di": (".diagram", "Diagram"), + "ERD": (".diagram", "Diagram"), + "diagram": (".diagram", None), # Return the module itself + # kill imports pymysql via connection + "kill": (".admin", "kill"), + # cli imports click + "cli": (".cli", "cli"), +} + + +def __getattr__(name: str): + """Lazy import for heavy dependencies.""" + if name in _lazy_modules: + module_path, attr_name = _lazy_modules[name] + import importlib + + module = importlib.import_module(module_path, __package__) + # If attr_name is None, return the module itself + attr = module if attr_name is None else getattr(module, attr_name) + # Cache in module __dict__ to avoid repeated __getattr__ calls + # and to override the submodule that importlib adds automatically + globals()[name] = attr + return attr + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/unit/test_lazy_imports.py b/tests/unit/test_lazy_imports.py new file mode 100644 index 000000000..7c1dc4c9e --- /dev/null +++ b/tests/unit/test_lazy_imports.py @@ -0,0 +1,121 @@ +""" +Tests for lazy import behavior. + +These tests verify that heavy dependencies (networkx, matplotlib, click) +are not loaded until their associated features are accessed. +""" + +import sys + + +def test_lazy_diagram_import(): + """Diagram module should not be loaded until dj.Diagram is accessed.""" + # Remove datajoint from sys.modules to get fresh import + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + # Import datajoint + import datajoint as dj + + # Diagram module should not be loaded yet + assert "datajoint.diagram" not in sys.modules, "diagram module loaded eagerly" + + # Access Diagram - should trigger lazy load + Diagram = dj.Diagram + assert "datajoint.diagram" in sys.modules, "diagram module not loaded after access" + assert Diagram.__name__ == "Diagram" + + +def test_lazy_admin_import(): + """Admin module should not be loaded until dj.kill is accessed.""" + # Remove datajoint from sys.modules to get fresh import + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + # Import datajoint + import datajoint as dj + + # Admin module should not be loaded yet + assert "datajoint.admin" not in sys.modules, "admin module loaded eagerly" + + # Access kill - should trigger lazy load + kill = dj.kill + assert "datajoint.admin" in sys.modules, "admin module not loaded after access" + assert callable(kill) + + +def test_lazy_cli_import(): + """CLI module should not be loaded until dj.cli is accessed.""" + # Remove datajoint from sys.modules to get fresh import + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + # Import datajoint + import datajoint as dj + + # CLI module should not be loaded yet + assert "datajoint.cli" not in sys.modules, "cli module loaded eagerly" + + # Access cli - should trigger lazy load and return the function + cli_func = dj.cli + assert "datajoint.cli" in sys.modules, "cli module not loaded after access" + assert callable(cli_func), "dj.cli should be callable (the cli function)" + + +def test_diagram_module_access(): + """dj.diagram should return the diagram module for accessing module-level attrs.""" + # Remove datajoint from sys.modules to get fresh import + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + import datajoint as dj + + # Access dj.diagram should return the module + diagram_module = dj.diagram + assert hasattr(diagram_module, "diagram_active"), "diagram module should have diagram_active" + assert hasattr(diagram_module, "Diagram"), "diagram module should have Diagram class" + + +def test_diagram_aliases(): + """Di and ERD should be aliases for Diagram.""" + # Remove datajoint from sys.modules to get fresh import + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + import datajoint as dj + + # All aliases should resolve to the same class + assert dj.Diagram is dj.Di + assert dj.Diagram is dj.ERD + + +def test_core_imports_available(): + """Core functionality should be available immediately after import.""" + # Remove datajoint from sys.modules to get fresh import + modules_to_remove = [key for key in sys.modules if key.startswith("datajoint")] + for mod in modules_to_remove: + del sys.modules[mod] + + import datajoint as dj + + # Core classes should be available without triggering lazy loads + assert hasattr(dj, "Schema") + assert hasattr(dj, "Table") + assert hasattr(dj, "Manual") + assert hasattr(dj, "Lookup") + assert hasattr(dj, "Computed") + assert hasattr(dj, "Imported") + assert hasattr(dj, "Part") + assert hasattr(dj, "Connection") + assert hasattr(dj, "config") + assert hasattr(dj, "errors") + + # Heavy modules should still not be loaded + assert "datajoint.diagram" not in sys.modules + assert "datajoint.admin" not in sys.modules + assert "datajoint.cli" not in sys.modules From 8732f87f2513f76efc8555286fa2a4d9ef53002a Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:53 -0700 Subject: [PATCH 204/219] fix: raise error when table declaration fails due to permissions (#1322) * fix: raise error when table declaration fails due to permissions Previously, AccessError during table declaration was silently swallowed, causing tables with cross-schema foreign keys to fail without any feedback when the user lacked REFERENCES privilege. Now: - If table already exists: suppress error (idempotent declaration) - If table doesn't exist: raise AccessError with helpful message about CREATE and REFERENCES privileges Closes #1161 Co-Authored-By: Claude Opus 4.5 * test: update test to expect AccessError at declaration time The test previously expected silent failure at declaration followed by error at insert time. Now we fail fast at declaration time (better UX). Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- src/datajoint/table.py | 11 +++++++++-- tests/integration/test_privileges.py | 19 ++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 346db97f9..e25f1cd98 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -148,8 +148,15 @@ def declare(self, context=None): try: self.connection.query(sql) except AccessError: - # skip if no create privilege - return + # Only suppress if table already exists (idempotent declaration) + # Otherwise raise - user needs to know about permission issues + if self.is_declared: + return + raise AccessError( + f"Cannot declare table {self.full_table_name}. " + f"Check that you have CREATE privilege on schema `{self.database}` " + f"and REFERENCES privilege on any referenced parent tables." + ) from None # Populate lineage table for this table's attributes self._populate_lineage(primary_key, fk_attribute_map) diff --git a/tests/integration/test_privileges.py b/tests/integration/test_privileges.py index ff5fc0c7f..0939823a0 100644 --- a/tests/integration/test_privileges.py +++ b/tests/integration/test_privileges.py @@ -90,18 +90,19 @@ def test_insert_failure(self, connection_djview, schema_any): UnprivilegedLanguage().insert1(("Socrates", "Greek")) def test_failure_to_create_table(self, connection_djview, schema_any): + """Table declaration should raise AccessError when user lacks CREATE privilege.""" unprivileged = dj.Schema(schema_any.database, namespace, connection=connection_djview) - @unprivileged - class Try(dj.Manual): - definition = """ # should not matter really - id : int - --- - value : float - """ + # Should raise AccessError at declaration time, not silently fail + with pytest.raises(dj.errors.AccessError): - with pytest.raises(dj.DataJointError): - Try().insert1((1, 1.5)) + @unprivileged + class Try(dj.Manual): + definition = """ # should not matter really + id : int + --- + value : float + """ class TestSubset: From 1aa68c025594a0f2add8b60b3cd274b792c104ca Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:51:59 -0700 Subject: [PATCH 205/219] fix: handle MATLAB cell arrays with empty/nested elements (#1323) Fix read_cell_array to handle edge cases from MATLAB: - Empty cell arrays ({}) - Cell arrays with empty elements ({[], [], []}) - Nested/ragged arrays ({[1,2], [3,4,5]}) - Cell matrices with mixed content The fix uses dtype='object' to avoid NumPy's array homogeneity requirements that caused reshape failures with ragged arrays. Closes #1056 Closes #1098 Co-authored-by: Claude Opus 4.5 --- src/datajoint/blob.py | 22 ++++++++- tests/integration/test_blob_matlab.py | 68 +++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/datajoint/blob.py b/src/datajoint/blob.py index 8651a57af..292350ad7 100644 --- a/src/datajoint/blob.py +++ b/src/datajoint/blob.py @@ -474,12 +474,30 @@ def pack_struct(self, array): ) # values def read_cell_array(self): - """deserialize MATLAB cell array""" + """ + Deserialize MATLAB cell array. + + Handles edge cases from MATLAB: + - Empty cell arrays ({}) + - Cell arrays with empty elements ({[], [], []}) + - Nested arrays ({[1,2], [3,4,5]}) - ragged arrays + - Cell matrices with mixed content + """ n_dims = self.read_value() shape = self.read_value(count=n_dims) n_elem = int(np.prod(shape)) result = [self.read_blob(n_bytes=self.read_value()) for _ in range(n_elem)] - return (self.squeeze(np.array(result).reshape(shape, order="F"), convert_to_scalar=False)).view(MatCell) + + # Handle empty cell array + if n_elem == 0: + return np.empty(0, dtype=object).view(MatCell) + + # Use object dtype to handle ragged/nested arrays without reshape errors. + # This avoids NumPy's array homogeneity requirements that cause failures + # with MATLAB cell arrays containing arrays of different sizes. + arr = np.empty(n_elem, dtype=object) + arr[:] = result + return self.squeeze(arr.reshape(shape, order="F"), convert_to_scalar=False).view(MatCell) def pack_cell_array(self, array): return ( diff --git a/tests/integration/test_blob_matlab.py b/tests/integration/test_blob_matlab.py index 630a9ac66..b7b05a0cb 100644 --- a/tests/integration/test_blob_matlab.py +++ b/tests/integration/test_blob_matlab.py @@ -160,3 +160,71 @@ def test_iter(schema_blob_pop): from_iter = {d["id"]: d for d in Blob()} assert len(from_iter) == len(Blob()) assert from_iter[1]["blob"] == "character string" + + +def test_cell_array_with_nested_arrays(): + """ + Test unpacking MATLAB cell arrays containing arrays of different sizes. + Regression test for issue #1098. + """ + # Create a cell array with nested arrays of different sizes (ragged) + cell = np.empty(2, dtype=object) + cell[0] = np.array([1, 2, 3]) + cell[1] = np.array([4, 5, 6, 7, 8]) + cell = cell.reshape((1, 2)).view(dj.MatCell) + + # Pack and unpack + packed = pack(cell) + unpacked = unpack(packed) + + # Should preserve structure + assert isinstance(unpacked, dj.MatCell) + assert unpacked.shape == (1, 2) + assert_array_equal(unpacked[0, 0], np.array([1, 2, 3])) + assert_array_equal(unpacked[0, 1], np.array([4, 5, 6, 7, 8])) + + +def test_cell_array_with_empty_elements(): + """ + Test unpacking MATLAB cell arrays containing empty arrays. + Regression test for issue #1056. + """ + # Create a cell array with empty elements: {[], [], []} + cell = np.empty(3, dtype=object) + cell[0] = np.array([]) + cell[1] = np.array([]) + cell[2] = np.array([]) + cell = cell.reshape((3, 1)).view(dj.MatCell) + + # Pack and unpack + packed = pack(cell) + unpacked = unpack(packed) + + # Should preserve structure + assert isinstance(unpacked, dj.MatCell) + assert unpacked.shape == (3, 1) + for i in range(3): + assert unpacked[i, 0].size == 0 + + +def test_cell_array_mixed_empty_nonempty(): + """ + Test unpacking MATLAB cell arrays with mixed empty and non-empty elements. + """ + # Create a cell array: {[1,2], [], [3,4,5]} + cell = np.empty(3, dtype=object) + cell[0] = np.array([1, 2]) + cell[1] = np.array([]) + cell[2] = np.array([3, 4, 5]) + cell = cell.reshape((3, 1)).view(dj.MatCell) + + # Pack and unpack + packed = pack(cell) + unpacked = unpack(packed) + + # Should preserve structure + assert isinstance(unpacked, dj.MatCell) + assert unpacked.shape == (3, 1) + assert_array_equal(unpacked[0, 0], np.array([1, 2])) + assert unpacked[1, 0].size == 0 + assert_array_equal(unpacked[2, 0], np.array([3, 4, 5])) From 6c29792027d67c5880a4f77ef40081e1535512e4 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 14:52:03 -0700 Subject: [PATCH 206/219] fix: provide helpful error when table heading is not configured (#1324) * fix: provide helpful error when table heading is not configured When using tables from non-activated schemas, operations that access the heading now raise a clear DataJointError instead of confusing "NoneType has no attribute" errors. Example: schema = dj.Schema() # Not activated @schema class MyTable(dj.Manual): ... MyTable().heading # Now raises: "Table `MyTable` is not properly # configured. Ensure the schema is activated..." Closes #1039 Co-Authored-By: Claude Opus 4.5 * fix: Allow heading introspection on base tier classes The heading property now returns None for base tier classes (Lookup, Manual, Imported, Computed, Part) instead of raising an error. This allows Python's help() and inspect modules to work correctly. User-defined table classes still get the helpful error message when trying to access heading on a non-activated schema. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- src/datajoint/table.py | 24 ++++++++++++++++++++++++ tests/integration/test_schema.py | 26 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/datajoint/table.py b/src/datajoint/table.py index e25f1cd98..77611cb59 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -120,6 +120,30 @@ def table_name(self): def class_name(self): return self.__class__.__name__ + # Base tier class names that should not raise errors when heading is None + _base_tier_classes = frozenset({"Table", "UserTable", "Lookup", "Manual", "Imported", "Computed", "Part"}) + + @property + def heading(self): + """ + Return the table's heading, or raise a helpful error if not configured. + + Overrides QueryExpression.heading to provide a clear error message + when the table is not properly associated with an activated schema. + For base tier classes (Lookup, Manual, etc.), returns None to support + introspection (e.g., help()). + """ + if self._heading is None: + # Don't raise error for base tier classes - they're used for introspection + if self.__class__.__name__ in self._base_tier_classes: + return None + raise DataJointError( + f"Table `{self.__class__.__name__}` is not properly configured. " + "Ensure the schema is activated before using the table. " + "Example: schema.activate('database_name') or schema = dj.Schema('database_name')" + ) + return self._heading + @property def definition(self): raise NotImplementedError("Subclasses of Table must implement the `definition` property") diff --git a/tests/integration/test_schema.py b/tests/integration/test_schema.py index d463ccf45..8cf231bf5 100644 --- a/tests/integration/test_schema.py +++ b/tests/integration/test_schema.py @@ -110,6 +110,32 @@ class UndecoratedClass(dj.Manual): print(a.full_table_name) +def test_non_activated_schema_heading_error(): + """ + Tables from non-activated schemas should raise informative errors. + Regression test for issue #1039. + """ + # Create schema without activating (no database name) + schema = dj.Schema() + + @schema + class TableA(dj.Manual): + definition = """ + id : int + --- + value : float + """ + + # Accessing heading should raise a helpful error + instance = TableA() + with pytest.raises(dj.DataJointError, match="not properly configured"): + _ = instance.heading + + # Operations that use heading should also raise helpful errors + with pytest.raises(dj.DataJointError, match="not properly configured"): + _ = instance.primary_key # Uses heading.primary_key + + def test_reject_decorated_part(schema_any): """ Decorating a dj.Part table should raise an informative exception. From 1a3e3d1bbbc6da2251c970a181913b6caebaec64 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 16:18:45 -0700 Subject: [PATCH 207/219] feat: Add URL representation for storage paths and remove orphaned docs (#1328) * feat: Add consistent URL representation for all storage paths (#1326) Implements unified URL handling for all storage backends including local files: - Add URL_PROTOCOLS tuple including file:// - Add is_url() to check if path is a URL - Add normalize_to_url() to convert local paths to file:// URLs - Add parse_url() to parse any URL into protocol and path - Add StorageBackend.get_url() to return full URLs for any backend - Add comprehensive unit tests for URL functions This enables consistent internal representation across all storage types, aligning with fsspec's unified approach to filesystems. Closes #1326 Co-Authored-By: Claude Opus 4.5 * test: Remove redundant URL tests from test_object.py The TestRemoteURLSupport class tested is_remote_url and parse_remote_url which were renamed to is_url and parse_url. These tests are now redundant as comprehensive coverage exists in tests/unit/test_storage_urls.py. Co-Authored-By: Claude Opus 4.5 * fix: Remove trailing whitespace from blank line in json.ipynb Co-Authored-By: Claude Opus 4.5 * style: Apply ruff formatting Co-Authored-By: Claude Opus 4.5 * chore: Remove accidentally committed local config files Co-Authored-By: Claude Opus 4.5 * style: Apply ruff formatting to test files Co-Authored-By: Claude Opus 4.5 * docs: Remove orphaned archive documentation Content has been migrated to datajoint-docs repository. Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- docs/src/archive/citation.md | 7 - docs/src/archive/client/credentials.md | 82 -- docs/src/archive/client/install.md | 209 ---- docs/src/archive/client/settings.md | 220 ---- .../archive/compute/autopopulate2.0-spec.md | 842 ------------- docs/src/archive/compute/distributed.md | 166 --- docs/src/archive/compute/key-source.md | 51 - docs/src/archive/compute/make.md | 215 ---- docs/src/archive/compute/populate.md | 317 ----- docs/src/archive/concepts/data-model.md | 172 --- docs/src/archive/concepts/data-pipelines.md | 166 --- docs/src/archive/concepts/principles.md | 136 --- docs/src/archive/concepts/teamwork.md | 97 -- docs/src/archive/concepts/terminology.md | 127 -- docs/src/archive/design/alter.md | 53 - docs/src/archive/design/diagrams.md | 110 -- docs/src/archive/design/drop.md | 23 - docs/src/archive/design/fetch-api-2.0-spec.md | 302 ----- .../design/hidden-job-metadata-spec.md | 355 ------ docs/src/archive/design/integrity.md | 218 ---- docs/src/archive/design/normalization.md | 117 -- docs/src/archive/design/pk-rules-spec.md | 318 ----- docs/src/archive/design/recall.md | 207 ---- docs/src/archive/design/schema.md | 49 - .../archive/design/semantic-matching-spec.md | 540 --------- docs/src/archive/design/tables/attach.md | 67 - docs/src/archive/design/tables/attributes.md | 181 --- docs/src/archive/design/tables/blobs.md | 26 - docs/src/archive/design/tables/codec-spec.md | 766 ------------ docs/src/archive/design/tables/codecs.md | 553 --------- docs/src/archive/design/tables/declare.md | 242 ---- .../src/archive/design/tables/dependencies.md | 241 ---- docs/src/archive/design/tables/filepath.md | 96 -- docs/src/archive/design/tables/indexes.md | 97 -- docs/src/archive/design/tables/lookup.md | 31 - docs/src/archive/design/tables/manual.md | 47 - docs/src/archive/design/tables/master-part.md | 112 -- docs/src/archive/design/tables/object.md | 357 ------ docs/src/archive/design/tables/primary.md | 178 --- .../design/tables/storage-types-spec.md | 892 -------------- docs/src/archive/design/tables/tiers.md | 68 -- docs/src/archive/faq.md | 192 --- docs/src/archive/images/StudentTable.png | Bin 48049 -> 0 bytes docs/src/archive/images/added-example-ERD.svg | 207 ---- docs/src/archive/images/data-engineering.png | Bin 63773 -> 0 bytes .../src/archive/images/data-science-after.png | Bin 38382 -> 0 bytes .../archive/images/data-science-before.png | Bin 23902 -> 0 bytes docs/src/archive/images/diff-example1.png | Bin 21912 -> 0 bytes docs/src/archive/images/diff-example2.png | Bin 22228 -> 0 bytes docs/src/archive/images/diff-example3.png | Bin 14483 -> 0 bytes docs/src/archive/images/dimitri-ERD.svg | 117 -- docs/src/archive/images/doc_1-1.png | Bin 3054 -> 0 bytes docs/src/archive/images/doc_1-many.png | Bin 5466 -> 0 bytes docs/src/archive/images/doc_many-1.png | Bin 5673 -> 0 bytes docs/src/archive/images/doc_many-many.png | Bin 6850 -> 0 bytes docs/src/archive/images/how-it-works.png | Bin 109082 -> 0 bytes .../src/archive/images/install-cmd-prompt.png | Bin 20046 -> 0 bytes .../archive/images/install-datajoint-1.png | Bin 10426 -> 0 bytes .../archive/images/install-datajoint-2.png | Bin 47943 -> 0 bytes docs/src/archive/images/install-git-1.png | Bin 17942 -> 0 bytes .../src/archive/images/install-graphviz-1.png | Bin 15790 -> 0 bytes .../archive/images/install-graphviz-2a.png | Bin 18167 -> 0 bytes .../archive/images/install-graphviz-2b.png | Bin 18189 -> 0 bytes docs/src/archive/images/install-jupyter-1.png | Bin 7213 -> 0 bytes docs/src/archive/images/install-jupyter-2.png | Bin 62158 -> 0 bytes .../src/archive/images/install-matplotlib.png | Bin 35368 -> 0 bytes docs/src/archive/images/install-pydotplus.png | Bin 7265 -> 0 bytes .../images/install-python-advanced-1.png | Bin 84133 -> 0 bytes .../images/install-python-advanced-2.png | Bin 82391 -> 0 bytes .../archive/images/install-python-simple.png | Bin 83717 -> 0 bytes .../archive/images/install-run-jupyter-1.png | Bin 25238 -> 0 bytes .../archive/images/install-run-jupyter-2.png | Bin 20153 -> 0 bytes .../images/install-verify-graphviz.png | Bin 8707 -> 0 bytes .../archive/images/install-verify-jupyter.png | Bin 7802 -> 0 bytes .../archive/images/install-verify-python.png | Bin 13897 -> 0 bytes docs/src/archive/images/join-example1.png | Bin 25783 -> 0 bytes docs/src/archive/images/join-example2.png | Bin 30178 -> 0 bytes docs/src/archive/images/join-example3.png | Bin 24993 -> 0 bytes .../archive/images/key_source_combination.png | Bin 18102 -> 0 bytes docs/src/archive/images/map-dataflow.png | Bin 171975 -> 0 bytes docs/src/archive/images/matched_tuples1.png | Bin 7598 -> 0 bytes docs/src/archive/images/matched_tuples2.png | Bin 8093 -> 0 bytes docs/src/archive/images/matched_tuples3.png | Bin 7753 -> 0 bytes docs/src/archive/images/mp-diagram.png | Bin 156543 -> 0 bytes docs/src/archive/images/op-restrict.png | Bin 45758 -> 0 bytes docs/src/archive/images/outer-example1.png | Bin 32099 -> 0 bytes docs/src/archive/images/pipeline-database.png | Bin 104258 -> 0 bytes docs/src/archive/images/pipeline.png | Bin 42094 -> 0 bytes docs/src/archive/images/python_collection.png | Bin 61544 -> 0 bytes .../images/queries_example_diagram.png | Bin 60116 -> 0 bytes .../archive/images/query_object_preview.png | Bin 95873 -> 0 bytes docs/src/archive/images/restrict-example1.png | Bin 23570 -> 0 bytes docs/src/archive/images/restrict-example2.png | Bin 24956 -> 0 bytes docs/src/archive/images/restrict-example3.png | Bin 13065 -> 0 bytes docs/src/archive/images/shapes_pipeline.svg | 36 - .../archive/images/spawned-classes-ERD.svg | 147 --- docs/src/archive/images/union-example1.png | Bin 11142 -> 0 bytes docs/src/archive/images/union-example2.png | Bin 13669 -> 0 bytes .../src/archive/images/virtual-module-ERD.svg | 147 --- docs/src/archive/manipulation/delete.md | 31 - docs/src/archive/manipulation/index.md | 9 - docs/src/archive/manipulation/insert.md | 173 --- docs/src/archive/manipulation/transactions.md | 36 - docs/src/archive/manipulation/update.md | 48 - docs/src/archive/publish-data.md | 34 - docs/src/archive/query/aggregation.md | 29 - docs/src/archive/query/example-schema.md | 112 -- docs/src/archive/query/fetch.md | 174 --- docs/src/archive/query/iteration.md | 36 - docs/src/archive/query/join.md | 37 - docs/src/archive/query/operators.md | 395 ------ docs/src/archive/query/principles.md | 81 -- docs/src/archive/query/project.md | 68 -- docs/src/archive/query/query-caching.md | 42 - docs/src/archive/query/restrict.md | 205 ---- docs/src/archive/query/union.md | 48 - docs/src/archive/query/universals.md | 46 - docs/src/archive/quick-start.md | 466 ------- docs/src/archive/sysadmin/bulk-storage.md | 104 -- docs/src/archive/sysadmin/database-admin.md | 364 ------ docs/src/archive/sysadmin/external-store.md | 293 ----- docs/src/archive/tutorials/dj-top.ipynb | 1015 ---------------- docs/src/archive/tutorials/json.ipynb | 1080 ----------------- src/datajoint/codecs.py | 6 +- src/datajoint/content_registry.py | 2 +- src/datajoint/heading.py | 6 +- src/datajoint/jobs.py | 2 +- src/datajoint/objectref.py | 26 - src/datajoint/settings.py | 7 +- src/datajoint/storage.py | 129 +- src/datajoint/table.py | 3 +- src/datajoint/user_tables.py | 4 +- tests/integration/test_object.py | 91 -- tests/unit/test_storage_urls.py | 121 ++ 134 files changed, 244 insertions(+), 14978 deletions(-) delete mode 100644 docs/src/archive/citation.md delete mode 100644 docs/src/archive/client/credentials.md delete mode 100644 docs/src/archive/client/install.md delete mode 100644 docs/src/archive/client/settings.md delete mode 100644 docs/src/archive/compute/autopopulate2.0-spec.md delete mode 100644 docs/src/archive/compute/distributed.md delete mode 100644 docs/src/archive/compute/key-source.md delete mode 100644 docs/src/archive/compute/make.md delete mode 100644 docs/src/archive/compute/populate.md delete mode 100644 docs/src/archive/concepts/data-model.md delete mode 100644 docs/src/archive/concepts/data-pipelines.md delete mode 100644 docs/src/archive/concepts/principles.md delete mode 100644 docs/src/archive/concepts/teamwork.md delete mode 100644 docs/src/archive/concepts/terminology.md delete mode 100644 docs/src/archive/design/alter.md delete mode 100644 docs/src/archive/design/diagrams.md delete mode 100644 docs/src/archive/design/drop.md delete mode 100644 docs/src/archive/design/fetch-api-2.0-spec.md delete mode 100644 docs/src/archive/design/hidden-job-metadata-spec.md delete mode 100644 docs/src/archive/design/integrity.md delete mode 100644 docs/src/archive/design/normalization.md delete mode 100644 docs/src/archive/design/pk-rules-spec.md delete mode 100644 docs/src/archive/design/recall.md delete mode 100644 docs/src/archive/design/schema.md delete mode 100644 docs/src/archive/design/semantic-matching-spec.md delete mode 100644 docs/src/archive/design/tables/attach.md delete mode 100644 docs/src/archive/design/tables/attributes.md delete mode 100644 docs/src/archive/design/tables/blobs.md delete mode 100644 docs/src/archive/design/tables/codec-spec.md delete mode 100644 docs/src/archive/design/tables/codecs.md delete mode 100644 docs/src/archive/design/tables/declare.md delete mode 100644 docs/src/archive/design/tables/dependencies.md delete mode 100644 docs/src/archive/design/tables/filepath.md delete mode 100644 docs/src/archive/design/tables/indexes.md delete mode 100644 docs/src/archive/design/tables/lookup.md delete mode 100644 docs/src/archive/design/tables/manual.md delete mode 100644 docs/src/archive/design/tables/master-part.md delete mode 100644 docs/src/archive/design/tables/object.md delete mode 100644 docs/src/archive/design/tables/primary.md delete mode 100644 docs/src/archive/design/tables/storage-types-spec.md delete mode 100644 docs/src/archive/design/tables/tiers.md delete mode 100644 docs/src/archive/faq.md delete mode 100644 docs/src/archive/images/StudentTable.png delete mode 100644 docs/src/archive/images/added-example-ERD.svg delete mode 100644 docs/src/archive/images/data-engineering.png delete mode 100644 docs/src/archive/images/data-science-after.png delete mode 100644 docs/src/archive/images/data-science-before.png delete mode 100644 docs/src/archive/images/diff-example1.png delete mode 100644 docs/src/archive/images/diff-example2.png delete mode 100644 docs/src/archive/images/diff-example3.png delete mode 100644 docs/src/archive/images/dimitri-ERD.svg delete mode 100644 docs/src/archive/images/doc_1-1.png delete mode 100644 docs/src/archive/images/doc_1-many.png delete mode 100644 docs/src/archive/images/doc_many-1.png delete mode 100644 docs/src/archive/images/doc_many-many.png delete mode 100644 docs/src/archive/images/how-it-works.png delete mode 100644 docs/src/archive/images/install-cmd-prompt.png delete mode 100644 docs/src/archive/images/install-datajoint-1.png delete mode 100644 docs/src/archive/images/install-datajoint-2.png delete mode 100644 docs/src/archive/images/install-git-1.png delete mode 100644 docs/src/archive/images/install-graphviz-1.png delete mode 100644 docs/src/archive/images/install-graphviz-2a.png delete mode 100644 docs/src/archive/images/install-graphviz-2b.png delete mode 100644 docs/src/archive/images/install-jupyter-1.png delete mode 100644 docs/src/archive/images/install-jupyter-2.png delete mode 100644 docs/src/archive/images/install-matplotlib.png delete mode 100644 docs/src/archive/images/install-pydotplus.png delete mode 100644 docs/src/archive/images/install-python-advanced-1.png delete mode 100644 docs/src/archive/images/install-python-advanced-2.png delete mode 100644 docs/src/archive/images/install-python-simple.png delete mode 100644 docs/src/archive/images/install-run-jupyter-1.png delete mode 100644 docs/src/archive/images/install-run-jupyter-2.png delete mode 100644 docs/src/archive/images/install-verify-graphviz.png delete mode 100644 docs/src/archive/images/install-verify-jupyter.png delete mode 100644 docs/src/archive/images/install-verify-python.png delete mode 100644 docs/src/archive/images/join-example1.png delete mode 100644 docs/src/archive/images/join-example2.png delete mode 100644 docs/src/archive/images/join-example3.png delete mode 100644 docs/src/archive/images/key_source_combination.png delete mode 100644 docs/src/archive/images/map-dataflow.png delete mode 100644 docs/src/archive/images/matched_tuples1.png delete mode 100644 docs/src/archive/images/matched_tuples2.png delete mode 100644 docs/src/archive/images/matched_tuples3.png delete mode 100644 docs/src/archive/images/mp-diagram.png delete mode 100644 docs/src/archive/images/op-restrict.png delete mode 100644 docs/src/archive/images/outer-example1.png delete mode 100644 docs/src/archive/images/pipeline-database.png delete mode 100644 docs/src/archive/images/pipeline.png delete mode 100644 docs/src/archive/images/python_collection.png delete mode 100644 docs/src/archive/images/queries_example_diagram.png delete mode 100644 docs/src/archive/images/query_object_preview.png delete mode 100644 docs/src/archive/images/restrict-example1.png delete mode 100644 docs/src/archive/images/restrict-example2.png delete mode 100644 docs/src/archive/images/restrict-example3.png delete mode 100644 docs/src/archive/images/shapes_pipeline.svg delete mode 100644 docs/src/archive/images/spawned-classes-ERD.svg delete mode 100644 docs/src/archive/images/union-example1.png delete mode 100644 docs/src/archive/images/union-example2.png delete mode 100644 docs/src/archive/images/virtual-module-ERD.svg delete mode 100644 docs/src/archive/manipulation/delete.md delete mode 100644 docs/src/archive/manipulation/index.md delete mode 100644 docs/src/archive/manipulation/insert.md delete mode 100644 docs/src/archive/manipulation/transactions.md delete mode 100644 docs/src/archive/manipulation/update.md delete mode 100644 docs/src/archive/publish-data.md delete mode 100644 docs/src/archive/query/aggregation.md delete mode 100644 docs/src/archive/query/example-schema.md delete mode 100644 docs/src/archive/query/fetch.md delete mode 100644 docs/src/archive/query/iteration.md delete mode 100644 docs/src/archive/query/join.md delete mode 100644 docs/src/archive/query/operators.md delete mode 100644 docs/src/archive/query/principles.md delete mode 100644 docs/src/archive/query/project.md delete mode 100644 docs/src/archive/query/query-caching.md delete mode 100644 docs/src/archive/query/restrict.md delete mode 100644 docs/src/archive/query/union.md delete mode 100644 docs/src/archive/query/universals.md delete mode 100644 docs/src/archive/quick-start.md delete mode 100644 docs/src/archive/sysadmin/bulk-storage.md delete mode 100644 docs/src/archive/sysadmin/database-admin.md delete mode 100644 docs/src/archive/sysadmin/external-store.md delete mode 100644 docs/src/archive/tutorials/dj-top.ipynb delete mode 100644 docs/src/archive/tutorials/json.ipynb create mode 100644 tests/unit/test_storage_urls.py diff --git a/docs/src/archive/citation.md b/docs/src/archive/citation.md deleted file mode 100644 index b5eb2d88b..000000000 --- a/docs/src/archive/citation.md +++ /dev/null @@ -1,7 +0,0 @@ -# Citation - -If your work uses the DataJoint for Python, please cite the following manuscript and Research Resource Identifier (RRID): - -- Yatsenko D, Reimer J, Ecker AS, Walker EY, Sinz F, Berens P, Hoenselaar A, Cotton RJ, Siapas AS, Tolias AS. DataJoint: managing big scientific data using MATLAB or Python. bioRxiv. 2015 Jan 1:031658. doi: https://doi.org/10.1101/031658 - -- DataJoint for Python - [RRID:SCR_014543](https://scicrunch.org/resolver/SCR_014543) - Version `Enter datajoint-python version you are using here` diff --git a/docs/src/archive/client/credentials.md b/docs/src/archive/client/credentials.md deleted file mode 100644 index 28e685f1f..000000000 --- a/docs/src/archive/client/credentials.md +++ /dev/null @@ -1,82 +0,0 @@ -# Credentials - -Database credentials should never be stored in config files. Use environment variables or a secrets directory instead. - -## Environment Variables (Recommended) - -Set the following environment variables: - -```bash -export DJ_HOST=db.example.com -export DJ_USER=alice -export DJ_PASS=secret -``` - -These take priority over all other configuration sources. - -## Secrets Directory - -Create a `.secrets/` directory next to your `datajoint.json`: - -``` -myproject/ -├── datajoint.json -└── .secrets/ - ├── database.user # Contains: alice - └── database.password # Contains: secret -``` - -Each file contains a single secret value (no JSON, just the raw value). - -Add `.secrets/` to your `.gitignore`: - -``` -# .gitignore -.secrets/ -``` - -## Docker / Kubernetes - -Mount secrets at `/run/secrets/datajoint/`: - -```yaml -# docker-compose.yml -services: - app: - volumes: - - ./secrets:/run/secrets/datajoint:ro -``` - -## Interactive Prompt - -If credentials are not provided via environment variables or secrets, DataJoint will prompt for them when connecting: - -```python ->>> import datajoint as dj ->>> dj.conn() -Please enter DataJoint username: alice -Please enter DataJoint password: -``` - -## Programmatic Access - -You can also set credentials in Python (useful for testing): - -```python -import datajoint as dj - -dj.config.database.user = "alice" -dj.config.database.password = "secret" -``` - -Note that `password` uses `SecretStr` internally, so it will be masked in logs and repr output. - -## Changing Database Password - -To change your database password, use your database's native tools: - -```sql -ALTER USER 'alice'@'%' IDENTIFIED BY 'new_password'; -``` - -Then update your environment variables or secrets file accordingly. diff --git a/docs/src/archive/client/install.md b/docs/src/archive/client/install.md deleted file mode 100644 index 18e6b79f4..000000000 --- a/docs/src/archive/client/install.md +++ /dev/null @@ -1,209 +0,0 @@ -# Install and Connect - -DataJoint is implemented for Python 3.10+. -You may install it from [PyPI](https://pypi.python.org/pypi/datajoint): - -```bash -pip3 install datajoint -``` - -or upgrade - -```bash -pip3 install --upgrade datajoint -``` - -## DataJoint Python Windows Install Guide - -This document outlines the steps necessary to install DataJoint on Windows for use in -connecting to a remote server hosting a DataJoint database. -Some limited discussion of installing MySQL is discussed in `MySQL for Windows`, but is -not covered in-depth since this is an uncommon usage scenario and not strictly required -to connect to DataJoint pipelines. - -### Quick steps - -Quick install steps for advanced users are as follows: - -- Install latest Python 3.x and ensure it is in `PATH` (3.10+ required) - ```bash - pip install datajoint - ``` - -For ERD drawing support: - -- Install Graphviz for Windows and ensure it is in `PATH` (64 bit builds currently -tested; URL below.) - ```bash - pip install pydotplus matplotlib - ``` - -Detailed instructions follow. - -### Step 1: install Python - -Python for Windows is available from: - -https://www.python.org/downloads/windows - -The latest 64 bit 3.x version (3.10 or later required) is available from the [Python site](https://www.python.org/downloads/windows/). - -From here run the installer to install Python. - -For a single-user machine, the regular installation process is sufficient - be sure to -select the `Add Python to PATH` option: - -![install-python-simple](../images/install-python-simple.png){: style="align:left"} - -For a shared machine, run the installer as administrator (right-click, run as -administrator) and select the advanced installation. -Be sure to select options as follows: - -![install-python-advanced-1](../images/install-python-advanced-1.png){: style="align:left"} -![install-python-advanced-2](../images/install-python-advanced-2.png){: style="align:left"} - -### Step 2: verify installation - -To verify the Python installation and make sure that your system is ready to install -DataJoint, open a command window by entering `cmd` into the Windows search bar: - -![install-cmd-prompt](../images/install-cmd-prompt.png){: style="align:left"} - -From here `python` and the Python package manager `pip` can be verified by running -`python -V` and `pip -V`, respectively: - -![install-verify-python](../images/install-verify-python.png){: style="align:left"} - -If you receive the error message that either `pip` or `python` is not a recognized -command, please uninstall Python and ensure that the option to add Python to the `PATH` -variable was properly configured. - -### Step 3: install DataJoint - -DataJoint (and other Python modules) can be easily installed using the `pip` Python -package manager which is installed as a part of Python and was verified in the previous -step. - -To install DataJoint simply run `pip install datajoint`: - -![install-datajoint-1](../images/install-datajoint-1.png){: style="align:left"} - -This will proceed to install DataJoint, along with several other required packages from -the PIP repository. -When finished, a summary of the activity should be presented: - -![install-datajoint-2](../images/install-datajoint-2.png){: style="align:left"} - -Note: You can find out more about the packages installed here and many other freely -available open source packages via [pypi](https://pypi.python.org/pypi), the Python -package index site. - -### (Optional) step 4: install packages for ERD support - -To draw diagrams of your DataJoint schema, the following additional steps should be -followed. - -#### Install Graphviz - -DataJoint currently utilizes [Graphviz](http://graphviz.org) to generate the ERD -visualizations. -Although a Windows version of Graphviz is available from the main site, it is an older -and out of date 32-bit version. -The recommended pre-release builds of the 64 bit version are available here: - -https://ci.appveyor.com/project/ellson/graphviz-pl238 - -More specifically, the build artifacts from the `Win64; Configuration: Release` are -recommended, available -[here](https://ci.appveyor.com/api/buildjobs/hlkclpfhf6gnakjq/artifacts/build%2FGraphviz-install.exe). - -This is a regular Windows installer executable, and will present a dialog when starting: - -![install-graphviz-1](../images/install-graphviz-1.png){: style="align:left"} - -It is important that an option to place Graphviz in the `PATH` be selected. - -For a personal installation: - -![install-graphviz-2a](../images/install-graphviz-2a.png){: style="align:left"} - -To install system wide: - -![install-graphviz-2b](../images/install-graphviz-2b.png){: style="align:left"} - -Once installed, Graphviz can be verified from a fresh command window as follows: - -![install-verify-graphviz](../images/install-verify-graphviz.png){: style="align:left"} - -If you receive the error message that the `dot` program is not a recognized command, -please uninstall Graphviz and ensure that the -option to add Python to the PATH variable was properly configured. - -Important: in some cases, running the `dot -c` command in a command prompt is required -to properly initialize the Graphviz installation. - -#### Install PyDotPlus - -The PyDotPlus library links the Graphviz installation to DataJoint and is easily -installed via `pip`: - -![install-pydotplus](../images/install-pydotplus.png){: style="align:left"} - -#### Install Matplotlib - -The Matplotlib library provides useful plotting utilities which are also used by -DataJoint's `Diagram` drawing facility. -The package is easily installed via `pip`: - -![install-matplotlib](../images/install-matplotlib.png){: style="align:left"} - -### (Optional) step 5: install Jupyter Notebook - -As described on the www.jupyter.org website: - -''' -The Jupyter Notebook is an open-source web application that allows -you to create and share documents that contain live code, equations, -visualizations and narrative text. -''' - -Although not a part of DataJoint, Jupyter Notebook can be a very useful tool for -building and interacting with DataJoint pipelines. -It is easily installed from `pip` as well: - -![install-jupyter-1](../images/install-jupyter-1.png){: style="align:left"} -![install-jupyter-2](../images/install-jupyter-2.png){: style="align:left"} - -Once installed, Jupyter Notebook can be started via the `jupyter notebook` command, -which should now be on your path: - -![install-verify-jupyter](../images/install-verify-jupyter.png){: style="align:left"} - -By default Jupyter Notebook will start a local private web server session from the -directory where it was started and start a web browser session connected to the session. - -![install-run-jupyter-1](../images/install-run-jupyter-1.png){: style="align:left"} -![install-run-jupyter-2](../images/install-run-jupyter-2.png){: style="align:left"} - -You now should be able to use the notebook viewer to navigate the filesystem and to -create new project folders and interactive Jupyter/Python/DataJoint notebooks. - -### Git for Windows - -The [Git](https://git-scm.com/) version control system is not a part of DataJoint but -is recommended for interacting with the broader Python/Git/GitHub sharing ecosystem. - -The Git for Windows installer is available from https://git-scm.com/download/win. - -![install-git-1](../images/install-git-1.png){: style="align:left"} - -The default settings should be sufficient and correct in most cases. - -### MySQL for Windows - -For hosting pipelines locally, the MySQL server package is required. - -MySQL for windows can be installed via the installers available from the -[MySQL website](https://dev.mysql.com/downloads/windows/). -Please note that although DataJoint should be fully compatible with a Windows MySQL -server installation, this mode of operation is not tested by the DataJoint team. diff --git a/docs/src/archive/client/settings.md b/docs/src/archive/client/settings.md deleted file mode 100644 index 40f4a6893..000000000 --- a/docs/src/archive/client/settings.md +++ /dev/null @@ -1,220 +0,0 @@ -# Configuration Settings - -DataJoint uses a type-checked configuration system built on [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/). - -## Configuration Sources - -Settings are loaded from the following sources (in priority order): - -1. **Environment variables** (`DJ_*`) -2. **Secrets directory** (`.secrets/` or `/run/secrets/datajoint/`) -3. **Project config file** (`datajoint.json`, searched recursively) -4. **Default values** - -## Project Structure - -``` -myproject/ -├── .git/ -├── datajoint.json # Project config (commit this) -├── .secrets/ # Local secrets (add to .gitignore) -│ ├── database.password -│ └── aws.secret_access_key -└── src/ - └── analysis.py # Config found via parent search -``` - -## Config File - -Create a `datajoint.json` file in your project root: - -```json -{ - "database": { - "host": "db.example.com", - "port": 3306 - }, - "stores": { - "raw": { - "protocol": "file", - "location": "/data/raw" - } - }, - "display": { - "limit": 20 - }, - "safemode": true -} -``` - -DataJoint searches for this file starting from the current directory and moving up through parent directories, stopping at the first `.git` or `.hg` directory (project boundary) or filesystem root. - -## Credentials - -**Never store credentials in config files.** Use one of these methods: - -### Environment Variables (Recommended) - -```bash -export DJ_USER=alice -export DJ_PASS=secret -export DJ_HOST=db.example.com -``` - -### Secrets Directory - -Create files in `.secrets/` next to your `datajoint.json`: - -``` -.secrets/ -├── database.password # Contains: secret -├── database.user # Contains: alice -├── aws.access_key_id -└── aws.secret_access_key -``` - -Add `.secrets/` to your `.gitignore`. - -For Docker/Kubernetes, secrets can be mounted at `/run/secrets/datajoint/`. - -## Accessing Settings - -```python -import datajoint as dj - -# Attribute access (preferred) -dj.config.database.host -dj.config.safemode - -# Dict-style access -dj.config["database.host"] -dj.config["safemode"] -``` - -## Temporary Overrides - -Use the context manager for temporary changes: - -```python -with dj.config.override(safemode=False): - # safemode is False here - table.delete() -# safemode is restored -``` - -For nested settings, use double underscores: - -```python -with dj.config.override(database__host="test.example.com"): - # database.host is temporarily changed - pass -``` - -## Available Settings - -### Database Connection - -| Setting | Environment Variable | Default | Description | -|---------|---------------------|---------|-------------| -| `database.host` | `DJ_HOST` | `localhost` | Database server hostname | -| `database.port` | `DJ_PORT` | `3306` | Database server port | -| `database.user` | `DJ_USER` | `None` | Database username | -| `database.password` | `DJ_PASS` | `None` | Database password (use env/secrets) | -| `database.reconnect` | — | `True` | Auto-reconnect on connection loss | -| `database.use_tls` | — | `None` | TLS mode: `True`, `False`, or `None` (auto) | - -### Display - -| Setting | Default | Description | -|---------|---------|-------------| -| `display.limit` | `12` | Max rows to display in previews | -| `display.width` | `14` | Column width in previews | -| `display.show_tuple_count` | `True` | Show total count in previews | - -### Other Settings - -| Setting | Default | Description | -|---------|---------|-------------| -| `safemode` | `True` | Prompt before destructive operations | -| `loglevel` | `INFO` | Logging level | -| `fetch_format` | `array` | Default fetch format (`array` or `frame`) | -| `enable_python_native_blobs` | `True` | Use Python-native blob serialization | - -## TLS Configuration - -DataJoint uses TLS by default if available. Control this with: - -```python -dj.config.database.use_tls = True # Require TLS -dj.config.database.use_tls = False # Disable TLS -dj.config.database.use_tls = None # Auto (default) -``` - -## External Storage - -Configure external stores in the `stores` section. See [External Storage](../sysadmin/external-store.md) for details. - -```json -{ - "stores": { - "raw": { - "protocol": "file", - "location": "/data/external" - } - } -} -``` - -## Object Storage - -Configure object storage for the [`object` type](../design/tables/object.md) in the `object_storage` section. This provides managed file and folder storage with fsspec backend support. - -### Local Filesystem - -```json -{ - "object_storage": { - "project_name": "my_project", - "protocol": "file", - "location": "/data/my_project" - } -} -``` - -### Amazon S3 - -```json -{ - "object_storage": { - "project_name": "my_project", - "protocol": "s3", - "bucket": "my-bucket", - "location": "my_project", - "endpoint": "s3.amazonaws.com" - } -} -``` - -### Object Storage Settings - -| Setting | Environment Variable | Required | Description | -|---------|---------------------|----------|-------------| -| `object_storage.project_name` | `DJ_OBJECT_STORAGE_PROJECT_NAME` | Yes | Unique project identifier | -| `object_storage.protocol` | `DJ_OBJECT_STORAGE_PROTOCOL` | Yes | Backend: `file`, `s3`, `gcs`, `azure` | -| `object_storage.location` | `DJ_OBJECT_STORAGE_LOCATION` | Yes | Base path or bucket prefix | -| `object_storage.bucket` | `DJ_OBJECT_STORAGE_BUCKET` | For cloud | Bucket name | -| `object_storage.endpoint` | `DJ_OBJECT_STORAGE_ENDPOINT` | For S3 | S3 endpoint URL | -| `object_storage.partition_pattern` | `DJ_OBJECT_STORAGE_PARTITION_PATTERN` | No | Path pattern with `{attr}` placeholders | -| `object_storage.token_length` | `DJ_OBJECT_STORAGE_TOKEN_LENGTH` | No | Random suffix length (default: 8) | -| `object_storage.access_key` | — | For cloud | Access key (use secrets) | -| `object_storage.secret_key` | — | For cloud | Secret key (use secrets) | - -### Object Storage Secrets - -Store cloud credentials in the secrets directory: - -``` -.secrets/ -├── object_storage.access_key -└── object_storage.secret_key -``` diff --git a/docs/src/archive/compute/autopopulate2.0-spec.md b/docs/src/archive/compute/autopopulate2.0-spec.md deleted file mode 100644 index 03382b06b..000000000 --- a/docs/src/archive/compute/autopopulate2.0-spec.md +++ /dev/null @@ -1,842 +0,0 @@ -# Autopopulate 2.0 Specification - -## Overview - -This specification redesigns the DataJoint job handling system to provide better visibility, control, and scalability for distributed computing workflows. The new system replaces the schema-level `~jobs` table with per-table job tables that offer richer status tracking, proper referential integrity, and dashboard-friendly monitoring. - -## Problem Statement - -### Current Jobs Table Limitations - -The existing `~jobs` table has significant limitations: - -1. **Limited status tracking**: Only supports `reserved`, `error`, and `ignore` statuses -2. **Functions as an error log**: Cannot efficiently track pending or completed jobs -3. **Poor dashboard visibility**: No way to monitor pipeline progress without querying multiple tables -4. **Key hashing obscures data**: Primary keys are stored as hashes, making debugging difficult -5. **No referential integrity**: Jobs table is independent of computed tables; orphaned jobs can accumulate - -### Key Source Limitations - -1. **Frequent manual modifications**: Subset operations require modifying `key_source` property -2. **Local visibility only**: Custom key sources are not accessible database-wide -3. **Performance bottleneck**: Multiple workers querying `key_source` simultaneously creates contention -4. **Codebase dependency**: Requires full pipeline codebase to determine pending work - -## Proposed Solution - -### Terminology - -- **Stale job**: A job (any status) whose key no longer exists in `key_source`. The upstream records have been deleted. Stale jobs are cleaned up by `refresh()` based on the `stale_timeout` parameter. - -- **Orphaned job**: A `reserved` job whose worker is no longer running. The process that reserved the job crashed, was terminated, or lost connection. The job remains `reserved` indefinitely. Orphaned jobs can be cleaned up by `refresh(orphan_timeout=...)` or manually deleted. - -- **Completed job**: A job with status `success`. Only exists when `keep_completed=True`. Represents historical record of successful computation. - -### Core Design Principles - -1. **Per-table jobs**: Each computed table gets its own hidden jobs table -2. **FK-only primary keys**: Auto-populated tables must have primary keys composed entirely of foreign key references. Non-FK primary key attributes are prohibited in new tables (legacy tables are supported with degraded granularity) -3. **No FK constraints on jobs**: Jobs tables omit foreign key constraints for performance; stale jobs are cleaned by `refresh()` -4. **Rich status tracking**: Extended status values for full lifecycle visibility -5. **Automatic refresh**: `populate()` automatically refreshes the jobs queue (adding new jobs, removing stale ones) -6. **Backward compatible**: When `reserve_jobs=False` (default), 1.0 behavior is preserved - -## Architecture - -### Jobs Table Structure - -Each `dj.Imported` or `dj.Computed` table `MyTable` will have an associated hidden jobs table `~~my_table` with the following structure: - -``` -# Job queue for MyTable -subject_id : int -session_id : int -... # Only FK-derived primary key attributes (NO foreign key constraints) ---- -status : enum('pending', 'reserved', 'success', 'error', 'ignore') -priority : uint8 # Lower = more urgent (0 = highest), set by refresh() -created_time=CURRENT_TIMESTAMP : timestamp # When job was added to queue -scheduled_time=CURRENT_TIMESTAMP : timestamp # Process on or after this time -reserved_time=null : timestamp # When job was reserved -completed_time=null : timestamp # When job completed -duration=null : float64 # Execution duration in seconds -error_message="" : varchar(2047) # Truncated error message -error_stack=null : # Full error traceback -user="" : varchar(255) # Database user who reserved/completed job -host="" : varchar(255) # Hostname of worker -pid=0 : uint32 # Process ID of worker -connection_id=0 : uint64 # MySQL connection ID -version="" : varchar(255) # Code version (git hash, package version, etc.) -``` - -**Important**: The jobs table primary key includes only those attributes that come through foreign keys in the target table's primary key. Additional primary key attributes (if any) are excluded. This means: -- If a target table has primary key `(-> Subject, -> Session, method)`, the jobs table has primary key `(subject_id, session_id)` only -- Multiple target rows may map to a single job entry when additional PK attributes exist -- Jobs tables have **no foreign key constraints** for performance (stale jobs handled by `refresh()`) - -### Access Pattern - -Jobs are accessed as a property of the computed table: - -```python -# Current pattern (schema-level) -schema.jobs - -# New pattern (per-table) -MyTable.jobs - -# Examples -FilteredImage.jobs # Access jobs table -FilteredImage.jobs & 'status="error"' # Query errors -FilteredImage.jobs.refresh() # Refresh job queue -``` - -### Status Values - -| Status | Description | -|--------|-------------| -| `pending` | Job is queued and ready to be processed | -| `reserved` | Job is currently being processed by a worker | -| `success` | Job completed successfully (optional, depends on settings) | -| `error` | Job failed with an error | -| `ignore` | Job should be skipped (manually set, not part of automatic transitions) | - -### Status Transitions - -```mermaid -stateDiagram-v2 - state "(none)" as none1 - state "(none)" as none2 - none1 --> pending : refresh() - none1 --> ignore : ignore() - pending --> reserved : reserve() - reserved --> none2 : complete() - reserved --> success : complete()* - reserved --> error : error() - success --> pending : refresh()* - error --> none2 : delete() - success --> none2 : delete() - ignore --> none2 : delete() -``` - -- `complete()` deletes the job entry (default when `jobs.keep_completed=False`) -- `complete()*` keeps the job as `success` (when `jobs.keep_completed=True`) -- `refresh()*` re-pends a `success` job if its key is in `key_source` but not in target - -**Transition methods:** -- `refresh()` — Adds new jobs as `pending`; also re-pends `success` jobs if key is in `key_source` but not in target -- `ignore()` — Marks a key as `ignore` (can be called on keys not yet in jobs table) -- `reserve()` — Marks a pending job as `reserved` before calling `make()` -- `complete()` — Marks reserved job as `success`, or deletes it (based on `jobs.keep_completed` setting) -- `error()` — Marks reserved job as `error` with message and stack trace -- `delete()` — Inherited from `delete_quick()`; use `(jobs & condition).delete()` pattern - -**Manual status control:** -- `ignore` is set manually via `jobs.ignore(key)` and is not part of automatic transitions -- Jobs with `status='ignore'` are skipped by `populate()` and `refresh()` -- To reset an ignored job, delete it and call `refresh()`: `jobs.ignored.delete(); jobs.refresh()` - -## API Design - -### JobsTable Class - -```python -class JobsTable(Table): - """Hidden table managing job queue for a computed table.""" - - @property - def definition(self) -> str: - """Dynamically generated based on parent table's primary key.""" - ... - - def refresh( - self, - *restrictions, - delay: float = 0, - priority: int = None, - stale_timeout: float = None, - orphan_timeout: float = None - ) -> dict: - """ - Refresh the jobs queue: add new jobs and clean up stale/orphaned jobs. - - Operations performed: - 1. Add new jobs: (key_source & restrictions) - target - jobs → insert as 'pending' - 2. Re-pend success jobs: if keep_completed=True and key in key_source but not in target - 3. Remove stale jobs: jobs older than stale_timeout whose keys no longer in key_source - 4. Remove orphaned jobs: reserved jobs older than orphan_timeout (if specified) - - Args: - restrictions: Conditions to filter key_source (for adding new jobs) - delay: Seconds from now until new jobs become available for processing. - Default: 0 (immediately available). Uses database server time. - priority: Priority for new jobs (lower = more urgent). - Default from config: jobs.default_priority (5) - stale_timeout: Seconds after which jobs are checked for staleness. - Jobs older than this are removed if key not in key_source. - Default from config: jobs.stale_timeout (3600s) - Set to 0 to skip stale cleanup. - orphan_timeout: Seconds after which reserved jobs are considered orphaned. - Reserved jobs older than this are deleted and re-added as pending. - Default: None (no orphan cleanup - must be explicit). - Typical value: 3600 (1 hour) or based on expected job duration. - - Returns: - { - 'added': int, # New pending jobs added - 'removed': int, # Stale jobs removed - 'orphaned': int, # Orphaned jobs reset to pending - 're_pended': int # Success jobs re-pended (keep_completed mode) - } - """ - ... - - def reserve(self, key: dict) -> bool: - """ - Attempt to reserve a job for processing. - - Updates status to 'reserved' if currently 'pending' and scheduled_time <= now. - No locking is used; rare conflicts are resolved by the make() transaction. - - Returns: - True if reservation successful, False if job not found or not pending. - """ - ... - - def complete(self, key: dict, duration: float = None) -> None: - """ - Mark a job as successfully completed. - - Updates status to 'success', records duration and completion time. - """ - ... - - def error(self, key: dict, error_message: str, error_stack: str = None) -> None: - """ - Mark a job as failed with error details. - - Updates status to 'error', records error message and stack trace. - """ - ... - - def ignore(self, key: dict) -> None: - """ - Mark a job to be ignored (skipped during populate). - - To reset an ignored job, delete it and call refresh(). - """ - ... - - # delete() is inherited from delete_quick() - no confirmation required - # Usage: (jobs & condition).delete() or jobs.errors.delete() - - @property - def pending(self) -> QueryExpression: - """Return query for pending jobs.""" - return self & 'status="pending"' - - @property - def reserved(self) -> QueryExpression: - """Return query for reserved jobs.""" - return self & 'status="reserved"' - - @property - def errors(self) -> QueryExpression: - """Return query for error jobs.""" - return self & 'status="error"' - - @property - def ignored(self) -> QueryExpression: - """Return query for ignored jobs.""" - return self & 'status="ignore"' - - @property - def completed(self) -> QueryExpression: - """Return query for completed jobs.""" - return self & 'status="success"' - - def progress(self) -> dict: - """ - Return job status breakdown. - - Returns: - { - 'pending': int, # Jobs waiting to be processed - 'reserved': int, # Jobs currently being processed - 'success': int, # Completed jobs (if keep_completed=True) - 'error': int, # Failed jobs - 'ignore': int, # Ignored jobs - 'total': int # Total jobs in table - } - """ - ... -``` - -### AutoPopulate Integration - -The `populate()` method is updated to use the new jobs table: - -```python -def populate( - self, - *restrictions, - suppress_errors: bool = False, - return_exception_objects: bool = False, - reserve_jobs: bool = False, - max_calls: int = None, - display_progress: bool = False, - processes: int = 1, - make_kwargs: dict = None, - # New parameters - priority: int = None, # Only process jobs at this priority or more urgent (lower values) - refresh: bool = None, # Refresh jobs queue before processing (default from config) -) -> dict: - """ - Populate the table by calling make() for each missing entry. - - Behavior depends on reserve_jobs parameter: - - When reserve_jobs=False (default, 1.0 compatibility mode): - - Jobs table is NOT used - - Keys computed directly from: (key_source & restrictions) - target - - No job reservation, no status tracking - - Suitable for single-worker scenarios - - When reserve_jobs=True (distributed mode): - 1. If refresh=True (or config['jobs.auto_refresh'] when refresh=None): - Call self.jobs.refresh(*restrictions) to sync jobs queue - 2. Fetch pending jobs ordered by (priority ASC, scheduled_time ASC) - Apply max_calls limit to fetched keys (total across all processes) - 3. For each pending job where scheduled_time <= now: - a. Mark job as 'reserved' - b. Call make(key) - c. On success: mark job as 'success' or delete (based on keep_completed) - d. On error: mark job as 'error' with message/stack - 4. Continue until all fetched jobs processed - - Args: - restrictions: Conditions to filter key_source - suppress_errors: If True, collect errors instead of raising - return_exception_objects: Return exception objects vs strings - reserve_jobs: Enable job reservation for distributed processing - max_calls: Maximum number of make() calls (total across all processes) - display_progress: Show progress bar - processes: Number of worker processes - make_kwargs: Non-computation kwargs passed to make() - priority: Only process jobs at this priority or more urgent (lower values) - refresh: Refresh jobs queue before processing. Default from config['jobs.auto_refresh'] - - Deprecated parameters (removed in 2.0): - - 'order': Job ordering now controlled by priority. Use refresh(priority=N). - - 'limit': Use max_calls instead. The distinction was confusing (see #1203). - - 'keys': Use restrictions instead. Direct key specification bypassed job tracking. - """ - ... -``` - -### Progress and Monitoring - -```python -# Current progress reporting -remaining, total = MyTable.progress() - -# Enhanced progress with jobs table -MyTable.jobs.progress() # Returns detailed status breakdown - -# Example output: -# { -# 'pending': 150, -# 'reserved': 3, -# 'success': 847, -# 'error': 12, -# 'ignore': 5, -# 'total': 1017 -# } -``` - -### Priority and Scheduling - -Priority and scheduling are handled via `refresh()` parameters. Lower priority values are more urgent (0 = highest priority). Scheduling uses relative time (seconds from now) based on database server time. - -```python -# Add urgent jobs (priority=0 is most urgent) -MyTable.jobs.refresh(priority=0) - -# Add normal jobs (default priority=5) -MyTable.jobs.refresh() - -# Add low-priority background jobs -MyTable.jobs.refresh(priority=10) - -# Schedule jobs for future processing (2 hours from now) -MyTable.jobs.refresh(delay=2*60*60) # 7200 seconds - -# Schedule jobs for tomorrow (24 hours from now) -MyTable.jobs.refresh(delay=24*60*60) - -# Combine: urgent jobs with 1-hour delay -MyTable.jobs.refresh(priority=0, delay=3600) - -# Add urgent jobs for specific subjects -MyTable.jobs.refresh(Subject & 'priority="urgent"', priority=0) -``` - -## Implementation Details - -### Table Naming Convention - -Jobs tables use the `~~` prefix (double tilde): -- Table `FilteredImage` (stored as `__filtered_image`) -- Jobs table: `~~filtered_image` (stored as `~~filtered_image`) - -The `~~` prefix distinguishes jobs tables from other hidden tables (`~jobs`, `~lineage`) while keeping names short. - -### Primary Key Constraint - -**New tables**: Auto-populated tables (`dj.Computed`, `dj.Imported`) must have primary keys composed entirely of foreign key references. Non-FK primary key attributes are prohibited. - -```python -# ALLOWED - all PK attributes come from foreign keys -@schema -class FilteredImage(dj.Computed): - definition = """ - -> Image - --- - filtered_image : - """ - -# ALLOWED - multiple FKs in primary key -@schema -class Analysis(dj.Computed): - definition = """ - -> Recording - -> AnalysisMethod # method comes from FK to lookup table - --- - result : float64 - """ - -# NOT ALLOWED - raises error on table declaration -@schema -class Analysis(dj.Computed): - definition = """ - -> Recording - method : varchar(32) # ERROR: non-FK primary key attribute - --- - result : float64 - """ -``` - -**Rationale**: This constraint ensures 1:1 correspondence between jobs and target rows, simplifying job status tracking and eliminating ambiguity. - -**Legacy table support**: Existing tables with non-FK primary key attributes continue to work. The jobs table uses only the FK-derived attributes, treating additional PK attributes as if they were secondary attributes. This means: -- One job entry may correspond to multiple target rows -- Job marked `success` when ANY matching target row exists -- Job marked `pending` only when NO matching target rows exist - -```python -# Legacy table (created before 2.0) -# Jobs table primary key: (recording_id) only -# One job covers all 'method' values for a given recording -@schema -class LegacyAnalysis(dj.Computed): - definition = """ - -> Recording - method : varchar(32) # Non-FK attribute (legacy, not recommended) - --- - result : float64 - """ -``` - -The jobs table has **no foreign key constraints** for performance reasons. - -### Stale Job Handling - -Stale jobs are jobs (any status except `ignore`) whose keys no longer exist in `key_source`. Since there are no FK constraints on jobs tables, these jobs remain until cleaned up by `refresh()`: - -```python -# refresh() handles stale jobs automatically -result = FilteredImage.jobs.refresh() -# Returns: {'added': 10, 'removed': 3, 'orphaned': 0, 're_pended': 0} - -# Stale detection logic: -# 1. Find jobs where created_time < (now - stale_timeout) -# 2. Check if their keys still exist in key_source -# 3. Remove jobs (pending, reserved, success, error) whose keys no longer exist -# 4. Jobs with status='ignore' are never removed (permanent until manual delete) -``` - -**Why not use foreign key cascading deletes?** -- FK constraints add overhead on every insert/update/delete operation -- Jobs tables are high-traffic (frequent reservations and status updates) -- Stale jobs are harmless until refresh—they simply won't match key_source -- The `refresh()` approach is more efficient for batch cleanup - -### Orphaned Job Handling - -Orphaned jobs are `reserved` jobs whose worker is no longer running. Unlike stale jobs, orphaned jobs reference valid keys—only the worker has disappeared. - -```python -# Automatic orphan cleanup (use with caution) -result = FilteredImage.jobs.refresh(orphan_timeout=3600) # 1 hour -# Jobs reserved more than 1 hour ago are deleted and re-added as pending -# Returns: {'added': 0, 'removed': 0, 'orphaned': 5, 're_pended': 0} - -# Manual orphan cleanup (more control) -(FilteredImage.jobs.reserved & 'reserved_time < NOW() - INTERVAL 2 HOUR').delete() -FilteredImage.jobs.refresh() # Re-adds as pending if key still in key_source -``` - -**When to use orphan_timeout**: -- In automated pipelines where job duration is predictable -- When workers are known to have failed (cluster node died) -- Set timeout > expected max job duration to avoid killing active jobs - -**When NOT to use orphan_timeout**: -- When job durations are highly variable -- When you need to coordinate with external orchestration -- Default is None (disabled) for safety - -### Table Drop and Alter Behavior - -When an auto-populated table is **dropped**, its associated jobs table is automatically dropped: - -```python -# Dropping FilteredImage also drops ~~filtered_image -FilteredImage.drop() -``` - -When an auto-populated table is **altered** (e.g., primary key changes), the jobs table is dropped and can be recreated via `refresh()`: - -```python -# Alter that changes primary key structure -# Jobs table is dropped since its structure no longer matches -FilteredImage.alter() - -# Recreate jobs table with new structure -FilteredImage.jobs.refresh() -``` - -### Lazy Table Creation - -Jobs tables are created automatically on first use: - -```python -# First call to populate with reserve_jobs=True creates the jobs table -FilteredImage.populate(reserve_jobs=True) -# Creates ~~filtered_image if it doesn't exist, then populates - -# Alternatively, explicitly create/refresh the jobs table -FilteredImage.jobs.refresh() -``` - -The jobs table is created with a primary key derived from the target table's foreign key attributes. - -### Conflict Resolution - -Conflict resolution relies on the transaction surrounding each `make()` call: - -- With `reserve_jobs=False`: Workers query `key_source` directly and may attempt the same key -- With `reserve_jobs=True`: Job reservation reduces conflicts but doesn't eliminate them entirely - -When two workers attempt to populate the same key: -1. Both workers attempt to reserve the same job (near-simultaneous) -2. Both reservation attempts succeed (no locking used) -3. Both call `make()` for the same key -4. First worker's `make()` transaction commits successfully -5. Second worker's `make()` transaction fails with duplicate key error -6. Second worker silently moves to next job (no status update) -7. First worker marks job `success` or deletes it - -**Important**: Only errors inside `make()` are logged with `error` status. Duplicate key errors from collisions are coordination artifacts handled silently—the first worker's completion takes precedence. - -**Edge case - first worker crashes after insert**: -- Job stays `reserved` (orphaned) -- Row exists in table (insert succeeded) -- Resolution: `refresh(orphan_timeout=...)` sees key exists in table, removes orphaned job - -**Why this is acceptable**: -- The `make()` transaction guarantees data integrity -- Duplicate key error is a clean, expected signal (not a real error) -- With `reserve_jobs=True`, conflicts are rare -- Wasted computation is minimal compared to locking complexity - -### Job Reservation vs Pre-Partitioning - -The job reservation mechanism (`reserve_jobs=True`) allows workers to dynamically claim jobs from a shared queue. However, some orchestration systems may prefer to **pre-partition** jobs before distributing them to workers: - -```python -# Pre-partitioning example: orchestrator divides work explicitly -all_pending = FilteredImage.jobs.pending.fetch("KEY") - -# Split jobs among workers (e.g., by worker index) -n_workers = 4 -for worker_id in range(n_workers): - worker_keys = all_pending[worker_id::n_workers] # Round-robin assignment - # Send worker_keys to worker via orchestration system (Slurm, K8s, etc.) - -# Worker receives its assigned keys and processes them directly -# Pass keys as restrictions to filter key_source -for key in assigned_keys: - FilteredImage.populate(key) # key acts as restriction, reserve_jobs=False by default -``` - -**When to use each approach**: - -| Approach | Use Case | -|----------|----------| -| **Dynamic reservation** (`reserve_jobs=True`) | Simple setups, variable job durations, workers that start/stop dynamically | -| **Pre-partitioning** | Batch schedulers (Slurm, PBS), predictable job counts, avoiding reservation overhead | - -Both approaches benefit from the same transaction-based conflict resolution as a safety net. - -## Configuration Options - -New configuration settings for job management: - -```python -# In datajoint config -dj.config['jobs.auto_refresh'] = True # Auto-refresh on populate (default: True) -dj.config['jobs.keep_completed'] = False # Keep success records (default: False) -dj.config['jobs.stale_timeout'] = 3600 # Seconds before pending job is considered stale (default: 3600) -dj.config['jobs.default_priority'] = 5 # Default priority for new jobs (lower = more urgent) -dj.config['jobs.version'] = None # Version string for jobs (default: None) - # Special values: 'git' = auto-detect git hash -``` - -### Config vs Parameter Precedence - -When both config and method parameters are available, **explicit parameters override config values**: - -```python -# Config sets defaults -dj.config['jobs.auto_refresh'] = True -dj.config['jobs.default_priority'] = 5 - -# Parameter overrides config -MyTable.populate(reserve_jobs=True, refresh=False) # refresh=False wins -MyTable.jobs.refresh(priority=0) # priority=0 wins -``` - -Parameters set to `None` use the config default. This allows per-call customization while maintaining global defaults. - -## Usage Examples - -### Basic Distributed Computing - -```python -# Worker 1 -FilteredImage.populate(reserve_jobs=True) - -# Worker 2 (can run simultaneously) -FilteredImage.populate(reserve_jobs=True) - -# Monitor progress -print(FilteredImage.jobs.progress()) -``` - -### Priority-Based Processing - -```python -# Add urgent jobs (priority=0 is most urgent) -urgent_subjects = Subject & 'priority="urgent"' -FilteredImage.jobs.refresh(urgent_subjects, priority=0) - -# Workers will process lowest-priority-value jobs first -FilteredImage.populate(reserve_jobs=True) -``` - -### Scheduled Processing - -```python -# Schedule jobs for overnight processing (8 hours from now) -FilteredImage.jobs.refresh('subject_id > 100', delay=8*60*60) - -# Only jobs whose scheduled_time <= now will be processed -FilteredImage.populate(reserve_jobs=True) -``` - -### Error Recovery - -```python -# View errors -errors = FilteredImage.jobs.errors.fetch(as_dict=True) -for err in errors: - print(f"Key: {err['subject_id']}, Error: {err['error_message']}") - -# Delete specific error jobs after fixing the issue -(FilteredImage.jobs & 'subject_id=42').delete() - -# Delete all error jobs -FilteredImage.jobs.errors.delete() - -# Re-add deleted jobs as pending (if keys still in key_source) -FilteredImage.jobs.refresh() -``` - -### Dashboard Queries - -```python -# Get pipeline-wide status using schema.jobs -def pipeline_status(schema): - return { - jt.table_name: jt.progress() - for jt in schema.jobs - } - -# Example output: -# { -# 'FilteredImage': {'pending': 150, 'reserved': 3, 'success': 847, 'error': 12}, -# 'Analysis': {'pending': 500, 'reserved': 0, 'success': 0, 'error': 0}, -# } - -# Refresh all jobs tables in the schema -for jobs_table in schema.jobs: - jobs_table.refresh() - -# Get all errors across the pipeline -all_errors = [] -for jt in schema.jobs: - errors = jt.errors.fetch(as_dict=True) - for err in errors: - err['_table'] = jt.table_name - all_errors.append(err) -``` - -## Backward Compatibility - -### Migration - -This is a major release. The legacy schema-level `~jobs` table is replaced by per-table jobs tables: - -- **Legacy `~jobs` table**: No longer used; can be dropped manually if present -- **New jobs tables**: Created automatically on first `populate(reserve_jobs=True)` call -- **No parallel support**: Teams should migrate cleanly to the new system - -### API Compatibility - -The `schema.jobs` property returns a list of all jobs table objects for auto-populated tables in the schema: - -```python -# Returns list of JobsTable objects -schema.jobs -# [FilteredImage.jobs, Analysis.jobs, ...] - -# Iterate over all jobs tables -for jobs_table in schema.jobs: - print(f"{jobs_table.table_name}: {jobs_table.progress()}") - -# Query all errors across the schema -all_errors = [job for jt in schema.jobs for job in jt.errors.fetch(as_dict=True)] - -# Refresh all jobs tables -for jobs_table in schema.jobs: - jobs_table.refresh() -``` - -This replaces the legacy single `~jobs` table with direct access to per-table jobs. - -## Hazard Analysis - -This section identifies potential hazards and their mitigations. - -### Race Conditions - -| Hazard | Description | Mitigation | -|--------|-------------|------------| -| **Simultaneous reservation** | Two workers reserve the same pending job at nearly the same time | Acceptable: duplicate `make()` calls are resolved by transaction—second worker gets duplicate key error | -| **Reserve during refresh** | Worker reserves a job while another process is running `refresh()` | No conflict: `refresh()` adds new jobs and removes stale ones; reservation updates existing rows | -| **Concurrent refresh calls** | Multiple processes call `refresh()` simultaneously | Acceptable: may result in duplicate insert attempts, but primary key constraint prevents duplicates | -| **Complete vs delete race** | One process completes a job while another deletes it | Acceptable: one operation succeeds, other becomes no-op (row not found) | - -### State Transitions - -| Hazard | Description | Mitigation | -|--------|-------------|------------| -| **Invalid state transition** | Code attempts illegal transition (e.g., pending → success) | Implementation enforces valid transitions; invalid attempts raise error | -| **Stuck in reserved** | Worker crashes while job is reserved (orphaned job) | Manual intervention required: `jobs.reserved.delete()` (see Orphaned Job Handling) | -| **Success re-pended unexpectedly** | `refresh()` re-pends a success job when user expected it to stay | Only occurs if `keep_completed=True` AND key exists in `key_source` but not in target; document clearly | -| **Ignore not respected** | Ignored jobs get processed anyway | Implementation must skip `status='ignore'` in `populate()` job fetching | - -### Data Integrity - -| Hazard | Description | Mitigation | -|--------|-------------|------------| -| **Stale job processed** | Job references deleted upstream data | `make()` will fail or produce invalid results; `refresh()` cleans stale jobs before processing | -| **Jobs table out of sync** | Jobs table doesn't match `key_source` | `refresh()` synchronizes; call periodically or rely on `populate(refresh=True)` | -| **Partial make failure** | `make()` partially succeeds then fails | DataJoint transaction rollback ensures atomicity; job marked as error | -| **Error message truncation** | Error details exceed `varchar(2047)` | Full stack stored in `error_stack` (mediumblob); `error_message` is summary only | - -### Performance - -| Hazard | Description | Mitigation | -|--------|-------------|------------| -| **Large jobs table** | Jobs table grows very large with `keep_completed=True` | Default is `keep_completed=False`; provide guidance on periodic cleanup | -| **Slow refresh on large key_source** | `refresh()` queries entire `key_source` | Can restrict refresh to subsets: `jobs.refresh(Subject & 'lab="smith"')` | -| **Many jobs tables per schema** | Schema with many computed tables has many jobs tables | Jobs tables are lightweight; only created on first use | - -### Operational - -| Hazard | Description | Mitigation | -|--------|-------------|------------| -| **Accidental job deletion** | User runs `jobs.delete()` without restriction | `delete()` inherits from `delete_quick()` (no confirmation); users must apply restrictions carefully | -| **Clearing active jobs** | User clears reserved jobs while workers are still running | May cause duplicated work if job is refreshed and picked up again; coordinate with orchestrator | -| **Priority confusion** | User expects higher number = higher priority | Document clearly: lower values are more urgent (0 = highest priority) | - -### Migration - -| Hazard | Description | Mitigation | -|--------|-------------|------------| -| **Legacy ~jobs table conflict** | Old `~jobs` table exists alongside new per-table jobs | Systems are independent; legacy table can be dropped manually | -| **Mixed version workers** | Some workers use old system, some use new | Major release; do not support mixed operation—require full migration | -| **Lost error history** | Migrating loses error records from legacy table | Document migration procedure; users can export legacy errors before migration | - -## Future Extensions - -- [ ] Web-based dashboard for job monitoring -- [ ] Webhook notifications for job completion/failure -- [ ] Job dependencies (job B waits for job A) -- [ ] Resource tagging (GPU required, high memory, etc.) -- [ ] Retry policies (max retries, exponential backoff) -- [ ] Job grouping/batching for efficiency -- [ ] Integration with external schedulers (Slurm, PBS, etc.) - -## Rationale - -### Why Not External Orchestration? - -The team considered integrating external tools like Airflow or Flyte but rejected this approach because: - -1. **Deployment complexity**: External orchestrators require significant infrastructure -2. **Maintenance burden**: Additional systems to maintain and monitor -3. **Accessibility**: Not all DataJoint users have access to orchestration platforms -4. **Tight integration**: DataJoint's transaction model requires close coordination - -The built-in jobs system provides 80% of the value with minimal additional complexity. - -### Why Per-Table Jobs? - -Per-table jobs tables provide: - -1. **Better isolation**: Jobs for one table don't affect others -2. **Simpler queries**: No need to filter by table_name -3. **Native keys**: Primary keys are readable, not hashed -4. **High performance**: No FK constraints means minimal overhead on job operations -5. **Scalability**: Each table's jobs can be indexed independently - -### Why Remove Key Hashing? - -The current system hashes primary keys to support arbitrary key types. The new system uses native keys because: - -1. **Readability**: Debugging is much easier with readable keys -2. **Query efficiency**: Native keys can use table indexes -3. **Foreign keys**: Hash-based keys cannot participate in foreign key relationships -4. **Simplicity**: No need for hash computation and comparison - -### Why FK-Derived Primary Keys Only? - -The jobs table primary key includes only attributes derived from foreign keys in the target table's primary key. This design: - -1. **Aligns with key_source**: The `key_source` query naturally produces keys matching the FK-derived attributes -2. **Simplifies job identity**: A job's identity is determined by its upstream dependencies -3. **Handles additional PK attributes**: When targets have additional PK attributes (e.g., `method`), one job covers all values for that attribute diff --git a/docs/src/archive/compute/distributed.md b/docs/src/archive/compute/distributed.md deleted file mode 100644 index 68c31f093..000000000 --- a/docs/src/archive/compute/distributed.md +++ /dev/null @@ -1,166 +0,0 @@ -# Distributed Computing - -## Job reservations - -Running `populate` on the same table on multiple computers will causes them to attempt -to compute the same data all at once. -This will not corrupt the data since DataJoint will reject any duplication. -One solution could be to cause the different computing nodes to populate the tables in -random order. -This would reduce some collisions but not completely prevent them. - -To allow efficient distributed computing, DataJoint provides a built-in job reservation -process. -When `dj.Computed` tables are auto-populated using job reservation, a record of each -ongoing computation is kept in a schema-wide `jobs` table, which is used internally by -DataJoint to coordinate the auto-population effort among multiple computing processes. - -Job reservations are activated by setting the keyword argument `reserve_jobs=True` in -`populate` calls. - -With job management enabled, the `make` method of each table class will also consult -the `jobs` table for reserved jobs as part of determining the next record to compute -and will create an entry in the `jobs` table as part of the attempt to compute the -resulting record for that key. -If the operation is a success, the record is removed. -In the event of failure, the job reservation entry is updated to indicate the details -of failure. -Using this simple mechanism, multiple processes can participate in the auto-population -effort without duplicating computational effort, and any errors encountered during the -course of the computation can be individually inspected to determine the cause of the -issue. - -As part of DataJoint, the jobs table can be queried using native DataJoint syntax. For -example, to list the jobs currently being run: - -```python -In [1]: schema.jobs -Out[1]: -*table_name *key_hash status error_message user host pid connection_id timestamp key error_stack -+------------+ +------------+ +----------+ +------------+ +------------+ +------------+ +-------+ +------------+ +------------+ +--------+ +------------+ -__job_results e4da3b7fbbce23 reserved datajoint@localhos localhost 15571 59 2017-09-04 14: -(2 tuples) -``` - -The above output shows that a record for the `JobResults` table is currently reserved -for computation, along with various related details of the reservation, such as the -MySQL connection ID, client user and host, process ID on the remote system, timestamp, -and the key for the record that the job is using for its computation. -Since DataJoint table keys can be of varying types, the key is stored in a binary -format to allow the table to store arbitrary types of record key data. -The subsequent sections will discuss querying the jobs table for key data. - -As mentioned above, jobs encountering errors during computation will leave their record -reservations in place, and update the reservation record with details of the error. - -For example, if a Python process is interrupted via the keyboard, a KeyboardError will -be logged to the database as follows: - -```python -In [2]: schema.jobs -Out[2]: -*table_name *key_hash status error_message user host pid connection_id timestamp key error_stack -+------------+ +------------+ +--------+ +------------+ +------------+ +------------+ +-------+ +------------+ +------------+ +--------+ +------------+ -__job_results 3416a75f4cea91 error KeyboardInterr datajoint@localhos localhost 15571 59 2017-09-04 14: -(1 tuples) -``` - -By leaving the job reservation record in place, the error can be inspected, and if -necessary the corresponding `dj.Computed` update logic can be corrected. -From there the jobs entry can be cleared, and the computation can then be resumed. -In the meantime, the presence of the job reservation will prevent this particular -record from being processed during subsequent auto-population calls. -Inspecting the job record for failure details can proceed much like any other DataJoint -query. - -For example, given the above table, errors can be inspected as follows: - -```python -In [3]: (schema.jobs & 'status="error"' ).fetch(as_dict=True) -Out[3]: -[OrderedDict([('table_name', '__job_results'), - ('key_hash', 'c81e728d9d4c2f636f067f89cc14862c'), - ('status', 'error'), - ('key', rec.array([(2,)], - dtype=[('id', 'O')])), - ('error_message', 'KeyboardInterrupt'), - ('error_stack', None), - ('user', 'datajoint@localhost'), - ('host', 'localhost'), - ('pid', 15571), - ('connection_id', 59), - ('timestamp', datetime.datetime(2017, 9, 4, 15, 3, 53))])] -``` - -This particular error occurred when processing the record with ID `2`, resulted from a -`KeyboardInterrupt`, and has no additional -error trace. - -After any system or code errors have been resolved, the table can simply be cleaned of -errors and the computation rerun. - -For example: - -```python -In [4]: (schema.jobs & 'status="error"' ).delete() -``` - -In some cases, it may be preferable to inspect the jobs table records using populate -keys. -Since job keys are hashed and stored as a blob in the jobs table to support the varying -types of keys, we need to query using the key hash instead of simply using the raw key -data. - -This can be done by using `dj.key_hash` to convert the key as follows: - -```python -In [4]: jk = {'table_name': JobResults.table_name, 'key_hash' : dj.key_hash({'id': 2})} - -In [5]: schema.jobs & jk -Out[5]: -*table_name *key_hash status key error_message error_stac user host pid connection_id timestamp -+------------+ +------------+ +--------+ +--------+ +------------+ +--------+ +------------+ +-------+ +--------+ +------------+ +------------+ -__job_results c81e728d9d4c2f error =BLOB= KeyboardInterr =BLOB= datajoint@localhost localhost 15571 59 2017-09-04 14: -(Total: 1) - -In [6]: (schema.jobs & jk).delete() - -In [7]: schema.jobs & jk -Out[7]: -*table_name *key_hash status key error_message error_stac user host pid connection_id timestamp -+------------+ +----------+ +--------+ +--------+ +------------+ +--------+ +------+ +------+ +-----+ +------------+ +-----------+ - -(Total: 0) -``` - -## Managing connections - -The DataJoint method `dj.kill` allows for viewing and termination of database -connections. -Restrictive conditions can be used to identify specific connections. -Restrictions are specified as strings and can involve any of the attributes of -`information_schema.processlist`: `ID`, `USER`, `HOST`, `DB`, `COMMAND`, `TIME`, -`STATE`, and `INFO`. - -Examples: - - `dj.kill('HOST LIKE "%compute%"')` lists only connections from hosts containing "compute". - `dj.kill('TIME > 600')` lists only connections older than 10 minutes. - -A list of connections meeting the restriction conditions (if present) are presented to -the user, along with the option to kill processes. By default, output is ordered by -ascending connection ID. To change the output order of dj.kill(), an additional -order_by argument can be provided. - -For example, to sort the output by hostname in descending order: - -```python -In [3]: dj.kill(None, None, 'host desc') -Out[3]: - ID USER HOST STATE TIME INFO -+--+ +----------+ +-----------+ +-----------+ +-----+ - 33 chris localhost:54840 1261 None - 17 chris localhost:54587 3246 None - 4 event_scheduler localhost Waiting on empty queue 187180 None -process to kill or "q" to quit > q -``` diff --git a/docs/src/archive/compute/key-source.md b/docs/src/archive/compute/key-source.md deleted file mode 100644 index c9b5d2ce7..000000000 --- a/docs/src/archive/compute/key-source.md +++ /dev/null @@ -1,51 +0,0 @@ -# Key Source - -## Default key source - -**Key source** refers to the set of primary key values over which -[autopopulate](./populate.md) iterates, calling the `make` method at each iteration. -Each `key` from the key source is passed to the table's `make` call. -By default, the key source for a table is the [join](../query/join.md) of its primary -[dependencies](../design/tables/dependencies.md). - -For example, consider a schema with three tables. -The `Stimulus` table contains one attribute `stimulus_type` with one of two values, -"Visual" or "Auditory". -The `Modality` table contains one attribute `modality` with one of three values, "EEG", -"fMRI", and "PET". -The `Protocol` table has primary dependencies on both the `Stimulus` and `Modality` tables. - -The key source for `Protocol` will then be all six combinations of `stimulus_type` and -`modality` as shown in the figure below. - -![Combination of stimulus_type and modality](../images/key_source_combination.png){: style="align:center"} - -## Custom key source - -A custom key source can be configured by setting the `key_source` property within a -table class, after the `definition` string. - -Any [query object](../query/fetch.md) can be used as the key source. -In most cases the new key source will be some alteration of the default key source. -Custom key sources often involve restriction to limit the key source to only relevant -entities. -Other designs may involve using only one of a table's primary dependencies. - -In the example below, the `EEG` table depends on the `Recording` table that lists all -recording sessions. -However, the `populate` method of `EEG` should only ingest recordings where the -`recording_type` is `EEG`. -Setting a custom key source prevents the `populate` call from iterating over recordings -of the wrong type. - -```python -@schema -class EEG(dj.Imported): -definition = """ --> Recording ---- -sample_rate : float -eeg_data : -""" -key_source = Recording & 'recording_type = "EEG"' -``` diff --git a/docs/src/archive/compute/make.md b/docs/src/archive/compute/make.md deleted file mode 100644 index 390be3b7b..000000000 --- a/docs/src/archive/compute/make.md +++ /dev/null @@ -1,215 +0,0 @@ -# Transactions in Make - -Each call of the [make](../compute/make.md) method is enclosed in a transaction. -DataJoint users do not need to explicitly manage transactions but must be aware of -their use. - -Transactions produce two effects: - -First, the state of the database appears stable within the `make` call throughout the -transaction: -two executions of the same query will yield identical results within the same `make` -call. - -Second, any changes to the database (inserts) produced by the `make` method will not -become visible to other processes until the `make` call completes execution. -If the `make` method raises an exception, all changes made so far will be discarded and -will never become visible to other processes. - -Transactions are particularly important in maintaining -[group integrity](../design/integrity.md#group-integrity) with -[master-part relationships](../design/tables/master-part.md). -The `make` call of a master table first inserts the master entity and then inserts all -the matching part entities in the part tables. -None of the entities become visible to other processes until the entire `make` call -completes, at which point they all become visible. - -### Three-Part Make Pattern for Long Computations - -For long-running computations, DataJoint provides an advanced pattern called the -**three-part make** that separates the `make` method into three distinct phases. -This pattern is essential for maintaining database performance and data integrity -during expensive computations. - -#### The Problem: Long Transactions - -Traditional `make` methods perform all operations within a single database transaction: - -```python -def make(self, key): - # All within one transaction - data = (ParentTable & key).fetch1() # Fetch - result = expensive_computation(data) # Compute (could take hours) - self.insert1(dict(key, result=result)) # Insert -``` - -This approach has significant limitations: -- **Database locks**: Long transactions hold locks on tables, blocking other operations -- **Connection timeouts**: Database connections may timeout during long computations -- **Memory pressure**: All fetched data must remain in memory throughout the computation -- **Failure recovery**: If computation fails, the entire transaction is rolled back - -#### The Solution: Three-Part Make Pattern - -The three-part make pattern splits the `make` method into three distinct phases, -allowing the expensive computation to occur outside of database transactions: - -```python -def make_fetch(self, key): - """Phase 1: Fetch all required data from parent tables""" - fetched_data = ((ParentTable1 & key).fetch1(), (ParentTable2 & key).fetch1()) - return fetched_data # must be a sequence, eg tuple or list - -def make_compute(self, key, *fetched_data): - """Phase 2: Perform expensive computation (outside transaction)""" - computed_result = expensive_computation(*fetched_data) - return computed_result # must be a sequence, eg tuple or list - -def make_insert(self, key, *computed_result): - """Phase 3: Insert results into the current table""" - self.insert1(dict(key, result=computed_result)) -``` - -#### Execution Flow - -To achieve data intensity without long transactions, the three-part make pattern follows this sophisticated execution sequence: - -```python -# Step 1: Fetch data outside transaction -fetched_data1 = self.make_fetch(key) -computed_result = self.make_compute(key, *fetched_data1) - -# Step 2: Begin transaction and verify data consistency -begin transaction: - fetched_data2 = self.make_fetch(key) - if fetched_data1 != fetched_data2: # deep comparison - cancel transaction # Data changed during computation - else: - self.make_insert(key, *computed_result) - commit_transaction -``` - -#### Key Benefits - -1. **Reduced Database Lock Time**: Only the fetch and insert operations occur within transactions, minimizing lock duration -2. **Connection Efficiency**: Database connections are only used briefly for data transfer -3. **Memory Management**: Fetched data can be processed and released during computation -4. **Fault Tolerance**: Computation failures don't affect database state -5. **Scalability**: Multiple computations can run concurrently without database contention - -#### Referential Integrity Protection - -The pattern includes a critical safety mechanism: **referential integrity verification**. -Before inserting results, the system: - -1. Re-fetches the source data within the transaction -2. Compares it with the originally fetched data using deep hashing -3. Only proceeds with insertion if the data hasn't changed - -This prevents the "phantom read" problem where source data changes during long computations, -ensuring that results remain consistent with their inputs. - -#### Implementation Details - -The pattern is implemented using Python generators in the `AutoPopulate` class: - -```python -def make(self, key): - # Step 1: Fetch data from parent tables - fetched_data = self.make_fetch(key) - computed_result = yield fetched_data - - # Step 2: Compute if not provided - if computed_result is None: - computed_result = self.make_compute(key, *fetched_data) - yield computed_result - - # Step 3: Insert the computed result - self.make_insert(key, *computed_result) - yield -``` -Therefore, it is possible to override the `make` method to implement the three-part make pattern by using the `yield` statement to return the fetched data and computed result as above. - -#### Use Cases - -This pattern is particularly valuable for: - -- **Machine learning model training**: Hours-long training sessions -- **Image processing pipelines**: Large-scale image analysis -- **Statistical computations**: Complex statistical analyses -- **Data transformations**: ETL processes with heavy computation -- **Simulation runs**: Time-consuming simulations - -#### Example: Long-Running Image Analysis - -Here's an example of how to implement the three-part make pattern for a -long-running image analysis task: - -```python -@schema -class ImageAnalysis(dj.Computed): - definition = """ - # Complex image analysis results - -> Image - --- - analysis_result : - processing_time : float - """ - - def make_fetch(self, key): - """Fetch the image data needed for analysis""" - image_data = (Image & key).fetch1('image') - params = (Params & key).fetch1('params') - return (image_data, params) # pack fetched_data - - def make_compute(self, key, image_data, params): - """Perform expensive image analysis outside transaction""" - import time - start_time = time.time() - - # Expensive computation that could take hours - result = complex_image_analysis(image_data, params) - processing_time = time.time() - start_time - return result, processing_time - - def make_insert(self, key, analysis_result, processing_time): - """Insert the analysis results""" - self.insert1(dict(key, - analysis_result=analysis_result, - processing_time=processing_time)) -``` - -The exact same effect may be achieved by overriding the `make` method as a generator function using the `yield` statement to return the fetched data and computed result as above: - -```python -@schema -class ImageAnalysis(dj.Computed): - definition = """ - # Complex image analysis results - -> Image - --- - analysis_result : - processing_time : float - """ - - def make(self, key): - image_data = (Image & key).fetch1('image') - params = (Params & key).fetch1('params') - computed_result = yield (image, params) # pack fetched_data - - if computed_result is None: - # Expensive computation that could take hours - import time - start_time = time.time() - result = complex_image_analysis(image_data, params) - processing_time = time.time() - start_time - computed_result = result, processing_time #pack - yield computed_result - - result, processing_time = computed_result # unpack - self.insert1(dict(key, - analysis_result=result, - processing_time=processing_time)) - yield # yield control back to the caller -``` -We expect that most users will prefer to use the three-part implementation over the generator function implementation due to its conceptual complexity. \ No newline at end of file diff --git a/docs/src/archive/compute/populate.md b/docs/src/archive/compute/populate.md deleted file mode 100644 index 91db7b176..000000000 --- a/docs/src/archive/compute/populate.md +++ /dev/null @@ -1,317 +0,0 @@ -# Auto-populate - -Auto-populated tables are used to define, execute, and coordinate computations in a -DataJoint pipeline. - -Tables in the initial portions of the pipeline are populated from outside the pipeline. -In subsequent steps, computations are performed automatically by the DataJoint pipeline -in auto-populated tables. - -Computed tables belong to one of the two auto-populated -[data tiers](../design/tables/tiers.md): `dj.Imported` and `dj.Computed`. -DataJoint does not enforce the distinction between imported and computed tables: the -difference is purely semantic, a convention for developers to follow. -If populating a table requires access to external files such as raw storage that is not -part of the database, the table is designated as **imported**. -Otherwise it is **computed**. - -Auto-populated tables are defined and queried exactly as other tables. -(See [Manual Tables](../design/tables/manual.md).) -Their data definition follows the same [definition syntax](../design/tables/declare.md). - -## Make - -For auto-populated tables, data should never be entered using -[insert](../manipulation/insert.md) directly. -Instead these tables must define the callback method `make(self, key)`. -The `insert` method then can only be called on `self` inside this callback method. - -Imagine that there is a table `test.Image` that contains 2D grayscale images in its -`image` attribute. -Let us define the computed table, `test.FilteredImage` that filters the image in some -way and saves the result in its `filtered_image` attribute. - -The class will be defined as follows. - -```python -@schema -class FilteredImage(dj.Computed): - definition = """ - # Filtered image - -> Image - --- - filtered_image : - """ - - def make(self, key): - img = (test.Image & key).fetch1('image') - key['filtered_image'] = myfilter(img) - self.insert1(key) -``` - -The `make` method receives one argument: the dict `key` containing the primary key -value of an element of [key source](key-source.md) to be worked on. - -The key represents the partially filled entity, usually already containing the -[primary key](../design/tables/primary.md) attributes of the key source. - -The `make` callback does three things: - -1. [Fetches](../query/fetch.md) data from tables upstream in the pipeline using the -`key` for [restriction](../query/restrict.md). -2. Computes and adds any missing attributes to the fields already in `key`. -3. Inserts the entire entity into `self`. - -A single `make` call may populate multiple entities when `key` does not specify the -entire primary key of the populated table, when the definition adds new attributes to the primary key. -This design is uncommon and not recommended. -The standard practice for autopopulated tables is to have its primary key composed of -foreign keys pointing to parent tables. - -### Three-Part Make Pattern for Long Computations - -For long-running computations, DataJoint provides an advanced pattern called the -**three-part make** that separates the `make` method into three distinct phases. -This pattern is essential for maintaining database performance and data integrity -during expensive computations. - -#### The Problem: Long Transactions - -Traditional `make` methods perform all operations within a single database transaction: - -```python -def make(self, key): - # All within one transaction - data = (ParentTable & key).fetch1() # Fetch - result = expensive_computation(data) # Compute (could take hours) - self.insert1(dict(key, result=result)) # Insert -``` - -This approach has significant limitations: -- **Database locks**: Long transactions hold locks on tables, blocking other operations -- **Connection timeouts**: Database connections may timeout during long computations -- **Memory pressure**: All fetched data must remain in memory throughout the computation -- **Failure recovery**: If computation fails, the entire transaction is rolled back - -#### The Solution: Three-Part Make Pattern - -The three-part make pattern splits the `make` method into three distinct phases, -allowing the expensive computation to occur outside of database transactions: - -```python -def make_fetch(self, key): - """Phase 1: Fetch all required data from parent tables""" - fetched_data = ((ParentTable & key).fetch1(),) - return fetched_data # must be a sequence, eg tuple or list - -def make_compute(self, key, *fetched_data): - """Phase 2: Perform expensive computation (outside transaction)""" - computed_result = expensive_computation(*fetched_data) - return computed_result # must be a sequence, eg tuple or list - -def make_insert(self, key, *computed_result): - """Phase 3: Insert results into the current table""" - self.insert1(dict(key, result=computed_result)) -``` - -#### Execution Flow - -To achieve data intensity without long transactions, the three-part make pattern follows this sophisticated execution sequence: - -```python -# Step 1: Fetch data outside transaction -fetched_data1 = self.make_fetch(key) -computed_result = self.make_compute(key, *fetched_data1) - -# Step 2: Begin transaction and verify data consistency -begin transaction: - fetched_data2 = self.make_fetch(key) - if fetched_data1 != fetched_data2: # deep comparison - cancel transaction # Data changed during computation - else: - self.make_insert(key, *computed_result) - commit_transaction -``` - -#### Key Benefits - -1. **Reduced Database Lock Time**: Only the fetch and insert operations occur within transactions, minimizing lock duration -2. **Connection Efficiency**: Database connections are only used briefly for data transfer -3. **Memory Management**: Fetched data can be processed and released during computation -4. **Fault Tolerance**: Computation failures don't affect database state -5. **Scalability**: Multiple computations can run concurrently without database contention - -#### Referential Integrity Protection - -The pattern includes a critical safety mechanism: **referential integrity verification**. -Before inserting results, the system: - -1. Re-fetches the source data within the transaction -2. Compares it with the originally fetched data using deep hashing -3. Only proceeds with insertion if the data hasn't changed - -This prevents the "phantom read" problem where source data changes during long computations, -ensuring that results remain consistent with their inputs. - -#### Implementation Details - -The pattern is implemented using Python generators in the `AutoPopulate` class: - -```python -def make(self, key): - # Step 1: Fetch data from parent tables - fetched_data = self.make_fetch(key) - computed_result = yield fetched_data - - # Step 2: Compute if not provided - if computed_result is None: - computed_result = self.make_compute(key, *fetched_data) - yield computed_result - - # Step 3: Insert the computed result - self.make_insert(key, *computed_result) - yield -``` -Therefore, it is possible to override the `make` method to implement the three-part make pattern by using the `yield` statement to return the fetched data and computed result as above. - -#### Use Cases - -This pattern is particularly valuable for: - -- **Machine learning model training**: Hours-long training sessions -- **Image processing pipelines**: Large-scale image analysis -- **Statistical computations**: Complex statistical analyses -- **Data transformations**: ETL processes with heavy computation -- **Simulation runs**: Time-consuming simulations - -#### Example: Long-Running Image Analysis - -Here's an example of how to implement the three-part make pattern for a -long-running image analysis task: - -```python -@schema -class ImageAnalysis(dj.Computed): - definition = """ - # Complex image analysis results - -> Image - --- - analysis_result : - processing_time : float - """ - - def make_fetch(self, key): - """Fetch the image data needed for analysis""" - return (Image & key).fetch1('image'), - - def make_compute(self, key, image_data): - """Perform expensive image analysis outside transaction""" - import time - start_time = time.time() - - # Expensive computation that could take hours - result = complex_image_analysis(image_data) - processing_time = time.time() - start_time - return result, processing_time - - def make_insert(self, key, analysis_result, processing_time): - """Insert the analysis results""" - self.insert1(dict(key, - analysis_result=analysis_result, - processing_time=processing_time)) -``` - -The exact same effect may be achieved by overriding the `make` method as a generator function using the `yield` statement to return the fetched data and computed result as above: - -```python -@schema -class ImageAnalysis(dj.Computed): - definition = """ - # Complex image analysis results - -> Image - --- - analysis_result : - processing_time : float - """ - - def make(self, key): - image_data = (Image & key).fetch1('image') - computed_result = yield (image_data, ) # pack fetched_data - - if computed_result is None: - # Expensive computation that could take hours - import time - start_time = time.time() - result = complex_image_analysis(image_data) - processing_time = time.time() - start_time - computed_result = result, processing_time #pack - yield computed_result - - result, processing_time = computed_result # unpack - self.insert1(dict(key, - analysis_result=result, - processing_time=processing_time)) - yield # yield control back to the caller -``` -We expect that most users will prefer to use the three-part implementation over the generator function implementation due to its conceptual complexity. - -## Populate - -The inherited `populate` method of `dj.Imported` and `dj.Computed` automatically calls -`make` for every key for which the auto-populated table is missing data. - -The `FilteredImage` table can be populated as - -```python -FilteredImage.populate() -``` - -The progress of long-running calls to `populate()` in datajoint-python can be -visualized by adding the `display_progress=True` argument to the populate call. - -Note that it is not necessary to specify which data needs to be computed. -DataJoint will call `make`, one-by-one, for every key in `Image` for which -`FilteredImage` has not yet been computed. - -Chains of auto-populated tables form computational pipelines in DataJoint. - -## Populate options - -The `populate` method accepts a number of optional arguments that provide more features -and allow greater control over the method's behavior. - -- `restrictions` - A list of restrictions, restricting as -`(tab.key_source & AndList(restrictions)) - tab.proj()`. - Here `target` is the table to be populated, usually `tab` itself. -- `suppress_errors` - If `True`, encountering an error will cancel the current `make` -call, log the error, and continue to the next `make` call. - Error messages will be logged in the job reservation table (if `reserve_jobs` is - `True`) and returned as a list. - See also `return_exception_objects` and `reserve_jobs`. - Defaults to `False`. -- `return_exception_objects` - If `True`, error objects are returned instead of error - messages. - This applies only when `suppress_errors` is `True`. - Defaults to `False`. -- `reserve_jobs` - If `True`, reserves job to indicate to other distributed processes. - The job reservation table may be access as `schema.jobs`. - Errors are logged in the jobs table. - Defaults to `False`. -- `order` - The order of execution, either `"original"`, `"reverse"`, or `"random"`. - Defaults to `"original"`. -- `display_progress` - If `True`, displays a progress bar. - Defaults to `False`. -- `limit` - If not `None`, checks at most this number of keys. - Defaults to `None`. -- `max_calls` - If not `None`, populates at most this many keys. - Defaults to `None`, which means no limit. - -## Progress - -The method `table.progress` reports how many `key_source` entries have been populated -and how many remain. -Two optional parameters allow more advanced use of the method. -A parameter of restriction conditions can be provided, specifying which entities to -consider. -A Boolean parameter `display` (default is `True`) allows disabling the output, such -that the numbers of remaining and total entities are returned but not printed. diff --git a/docs/src/archive/concepts/data-model.md b/docs/src/archive/concepts/data-model.md deleted file mode 100644 index 90460361a..000000000 --- a/docs/src/archive/concepts/data-model.md +++ /dev/null @@ -1,172 +0,0 @@ -# Data Model - -## What is a data model? - -A **data model** is a conceptual framework that defines how data is organized, -represented, and transformed. It gives us the components for creating blueprints for the -structure and operations of data management systems, ensuring consistency and efficiency -in data handling. - -Data management systems are built to accommodate these models, allowing us to manage -data according to the principles laid out by the model. If you’re studying data science -or engineering, you’ve likely encountered different data models, each providing a unique -approach to organizing and manipulating data. - -A data model is defined by considering the following key aspects: - -+ What are the fundamental elements used to structure the data? -+ What operations are available for defining, creating, and manipulating the data? -+ What mechanisms exist to enforce the structure and rules governing valid data interactions? - -## Types of data models - -Among the most familiar data models are those based on files and folders: data of any -kind are lumped together into binary strings called **files**, files are collected into -folders, and folders can be nested within other folders to create a folder hierarchy. - -Another family of data models are various **tabular models**. -For example, items in CSV files are listed in rows, and the attributes of each item are -stored in columns. -Various **spreadsheet** models allow forming dependencies between cells and groups of -cells, including complex calculations. - -The **object data model** is common in programming, where data are represented as -objects in memory with properties and methods for transformations of such data. - -## Relational data model - -The **relational model** is a way of thinking about data as sets and operations on sets. -Formalized almost a half-century ago ([Codd, -1969](https://dl.acm.org/citation.cfm?doid=362384.362685)). The relational data model is -one of the most powerful and precise ways to store and manage structured data. At its -core, this model organizes all data into tables--representing mathematical -relations---where each table consists of rows (representing mathematical tuples) and -columns (often called attributes). - -### Core principles of the relational data model - -**Data representation:** - Data are represented and manipulated in the form of relations. - A relation is a set (i.e. an unordered collection) of entities of values for each of - the respective named attributes of the relation. - Base relations represent stored data while derived relations are formed from base - relations through query expressions. - A collection of base relations with their attributes, domain constraints, uniqueness - constraints, and referential constraints is called a schema. - -**Domain constraints:** - Each attribute (column) in a table is associated with a specific attribute domain (or - datatype, a set of possible values), ensuring that the data entered is valid. - Attribute domains may not include relations, which keeps the data model - flat, i.e. free of nested structures. - -**Uniqueness constraints:** - Entities within relations are addressed by values of their attributes. - To identify and relate data elements, uniqueness constraints are imposed on subsets - of attributes. - Such subsets are then referred to as keys. - One key in a relation is designated as the primary key used for referencing its elements. - -**Referential constraints:** - Associations among data are established by means of referential constraints with the - help of foreign keys. - A referential constraint on relation A referencing relation B allows only those - entities in A whose foreign key attributes match the key attributes of an entity in B. - -**Declarative queries:** - Data queries are formulated through declarative, as opposed to imperative, - specifications of sought results. - This means that query expressions convey the logic for the result rather than the - procedure for obtaining it. - Formal languages for query expressions include relational algebra, relational - calculus, and SQL. - -The relational model has many advantages over both hierarchical file systems and -tabular models for maintaining data integrity and providing flexible access to -interesting subsets of the data. - -Popular implementations of the relational data model rely on the Structured Query -Language (SQL). -SQL comprises distinct sublanguages for schema definition, data manipulation, and data -queries. -SQL thoroughly dominates in the space of relational databases and is often conflated -with the relational data model in casual discourse. -Various terminologies are used to describe related concepts from the relational data -model. -Similar to spreadsheets, relations are often visualized as tables with *attributes* -corresponding to *columns* and *entities* corresponding to *rows*. -In particular, SQL uses the terms *table*, *column*, and *row*. - -## The DataJoint Model - -DataJoint is a conceptual refinement of the relational data model offering a more -expressive and rigorous framework for database programming ([Yatsenko et al., -2018](https://arxiv.org/abs/1807.11104)). The DataJoint model facilitates conceptual -clarity, efficiency, workflow management, and precise and flexible data -queries. By enforcing entity normalization, -simplifying dependency declarations, offering a rich query algebra, and visualizing -relationships through schema diagrams, DataJoint makes relational database programming -more intuitive and robust for complex data pipelines. - -The model has emerged over a decade of continuous development of complex data -pipelines for neuroscience experiments ([Yatsenko et al., -2015](https://www.biorxiv.org/content/early/2015/11/14/031658)). DataJoint has allowed -researchers with no prior knowledge of databases to collaborate effectively on common -data pipelines sustaining data integrity and supporting flexible access. DataJoint is -currently implemented as client libraries in MATLAB and Python. These libraries work by -transpiling DataJoint queries into SQL before passing them on to conventional relational -database systems that serve as the backend, in combination with bulk storage systems for -storing large contiguous data objects. - -DataJoint comprises: - -+ a schema [definition](../design/tables/declare.md) language -+ a data [manipulation](../manipulation/index.md) language -+ a data [query](../query/principles.md) language -+ a [diagramming](../design/diagrams.md) notation for visualizing relationships between -modeled entities - -The key refinement of DataJoint over other relational data models and their -implementations is DataJoint's support of -[entity normalization](../design/normalization.md). - -### Core principles of the DataJoint model - -**Entity Normalization** - DataJoint enforces entity normalization, ensuring that every entity set (table) is - well-defined, with each element belonging to the same type, sharing the same - attributes, and distinguished by the same primary key. This principle reduces - redundancy and avoids data anomalies, similar to Boyce-Codd Normal Form, but with a - more intuitive structure than traditional SQL. - -**Simplified Schema Definition and Dependency Management** - DataJoint introduces a schema definition language that is more expressive and less - error-prone than SQL. Dependencies are explicitly declared using arrow notation - (->), making referential constraints easier to understand and visualize. The - dependency structure is enforced as an acyclic directed graph, which simplifies - workflows by preventing circular dependencies. - -**Integrated Query Operators producing a Relational Algebra** - DataJoint introduces five query operators (restrict, join, project, aggregate, and - union) with algebraic closure, allowing them to be combined seamlessly. These - operators are designed to maintain operational entity normalization, ensuring query - outputs remain valid entity sets. - -**Diagramming Notation for Conceptual Clarity** - DataJoint’s schema diagrams simplify the representation of relationships between - entity sets compared to ERM diagrams. Relationships are expressed as dependencies - between entity sets, which are visualized using solid or dashed lines for primary - and secondary dependencies, respectively. - -**Unified Logic for Binary Operators** - DataJoint simplifies binary operations by requiring attributes involved in joins or - comparisons to be homologous (i.e., sharing the same origin). This avoids the - ambiguity and pitfalls of natural joins in SQL, ensuring more predictable query - results. - -**Optimized Data Pipelines for Scientific Workflows** - DataJoint treats the database as a data pipeline where each entity set defines a - step in the workflow. This makes it ideal for scientific experiments and complex - data processing, such as in neuroscience. Its MATLAB and Python libraries transpile - DataJoint queries into SQL, bridging the gap between scientific programming and - relational databases. diff --git a/docs/src/archive/concepts/data-pipelines.md b/docs/src/archive/concepts/data-pipelines.md deleted file mode 100644 index cf20b075b..000000000 --- a/docs/src/archive/concepts/data-pipelines.md +++ /dev/null @@ -1,166 +0,0 @@ -# Data Pipelines - -## What is a data pipeline? - -A scientific **data pipeline** is a collection of processes and systems for organizing -the data, computations, and workflows used by a research group as they jointly perform -complex sequences of data acquisition, processing, and analysis. - -A variety of tools can be used for supporting shared data pipelines: - -Data repositories - Research teams set up a shared **data repository**. - This minimal data management tool allows depositing and retrieving data and managing - user access. - For example, this may include a collection of files with standard naming conventions - organized into folders and sub-folders. - Or a data repository might reside on the cloud, for example in a collection of S3 - buckets. - This image of data management -- where files are warehoused and retrieved from a - hierarchically-organized system of folders -- is an approach that is likely familiar - to most scientists. - -Database systems - **Databases** are a form of data repository providing additional capabilities: - - 1. Defining, communicating, and enforcing structure in the stored data. - 2. Maintaining data integrity: correct identification of data and consistent cross-references, dependencies, and groupings among the data. - 3. Supporting queries that retrieve various cross-sections and transformation of the deposited data. - - Most scientists have some familiarity with these concepts, for example the notion of maintaining consistency between data and the metadata that describes it, or applying a filter to an Excel spreadsheet to retrieve specific subsets of information. - However, usually the more advanced concepts involved in building and using relational databases fall under the specific expertise of data scientists. - -Data pipelines - **Data pipeline** frameworks may include all the features of a database system along - with additional functionality: - - 1. Integrating computations to perform analyses and manage intermediate results in a principled way. - 2. Supporting distributed computations without conflict. - 3. Defining, communicating, and enforcing **workflow**, making clear the sequence of steps that must be performed for data entry, acquisition, and processing. - - Again, the informal notion of an analysis "workflow" will be familiar to most scientists, along with the logistical difficulties associated with managing a workflow that is shared by multiple scientists within or across labs. - - Therefore, a full-featured data pipeline framework may also be described as a [scientific workflow system](https://en.wikipedia.org/wiki/Scientific_workflow_system). - -Major features of data management frameworks: data repositories, databases, and data pipelines. - -![data pipelines vs databases vs data repositories](../images/pipeline-database.png){: style="align:center"} - -## What is DataJoint? - -DataJoint is a free open-source framework for creating scientific data pipelines -directly from MATLAB or Python (or any mixture of the two). -The data are stored in a language-independent way that allows interoperability between -MATLAB and Python, with additional languages in the works. -DataJoint pipelines become the central tool in the operations of data-intensive labs or -consortia as they organize participants with different roles and skills around a common -framework. - -In DataJoint, a data pipeline is a sequence of steps (more generally, a directed -acyclic graph) with integrated data storage at each step. -The pipeline may have some nodes requiring manual data entry or import from external -sources, some that read from raw data files, and some that perform computations on data -stored in other database nodes. -In a typical scenario, experimenters and acquisition instruments feed data into nodes -at the head of the pipeline, while downstream nodes perform automated computations for -data processing and analysis. - -For example, this is the pipeline for a simple mouse experiment involving calcium -imaging in mice. - -![A data pipeline](../images/pipeline.png){: style="width:250px; align:center"} - -In this example, the experimenter first enters information about a mouse, then enters -information about each imaging session in that mouse, and then each scan performed in -each imaging session. -Next the automated portion of the pipeline takes over to import the raw imaging data, -perform image alignment to compensate for motion, image segmentation to identify cells -in the images, and extraction of calcium traces. -Finally, the receptive field (RF) computation is performed by relating the calcium -signals to the visual stimulus information. - -## How DataJoint works - -DataJoint enables data scientists to build and operate scientific data pipelines. - -Conceptual overview of DataJoint operation. - -![DataJoint operation](../images/how-it-works.png){: style="align:center"} - -DataJoint provides a simple and powerful data model, which is detailed more formally in [Yatsenko D, Walker EY, Tolias AS (2018). DataJoint: A Simpler Relational Data Model.](https://arxiv.org/abs/1807.11104). -Put most generally, a "data model" defines how to think about data and the operations -that can be performed on them. -DataJoint's model is a refinement of the relational data model: all nodes in the -pipeline are simple tables storing data, tables are related by their shared attributes, -and query operations can combine the contents of multiple tables. -DataJoint enforces specific constraints on the relationships between tables that help -maintain data integrity and enable flexible access. -DataJoint uses a succinct data definition language, a powerful data query language, and -expressive visualizations of the pipeline. -A well-defined and principled approach to data organization and computation enables -teams of scientists to work together efficiently. -The data become immediately available to all participants with appropriate access privileges. -Some of the "participants" may be computational agents that perform processing and -analysis, and so DataJoint features a built-in distributed job management process to -allow distributing analysis between any number of computers. - -From a practical point of view, the back-end data architecture may vary depending on -project requirements. -Typically, the data architecture includes a relational database server (e.g. MySQL) and -a bulk data storage system (e.g. [AWS S3](https://aws.amazon.com/s3/) or a filesystem). -However, users need not interact with the database directly, but via MATLAB or Python -objects that are each associated with an individual table in the database. -One of the main advantages of this approach is that DataJoint clearly separates the -data model facing the user from the data architecture implementing data management and -computing. DataJoint works well in combination with good code sharing (e.g. with -[git](https://git-scm.com/)) and environment sharing (e.g. with -[Docker](https://www.docker.com/)). - -DataJoint is designed for quick prototyping and continuous exploration as experimental -designs change or evolve. -New analysis methods can be added or removed at any time, and the structure of the -workflow itself can change over time, for example as new data acquisition methods are -developed. - -With DataJoint, data sharing and publishing is no longer a separate step at the end of -the project. -Instead data sharing is an inherent feature of the process: to share data with other -collaborators or to publish the data to the world, one only needs to set the access -privileges. - -## Real-life example - -The [Mesoscale Activity Project](https://www.simonsfoundation.org/funded-project/%20multi-regional-neuronal-dynamics-of-memory-guided-flexible-behavior/) -(MAP) is a collaborative project between four neuroscience labs. -MAP uses DataJoint for data acquisition, processing, analysis, interfaces, and external sharing. - -The DataJoint pipeline for the MAP project. - -![A data pipeline for the MAP project](../images/map-dataflow.png){: style="align:center"} - -The pipeline is hosted in the cloud through [Amazon Web Services](https://aws.amazon.com/) (AWS). -MAP data scientists at the Janelia Research Campus and Baylor College of Medicine -defined the data pipeline. -Experimental scientists enter manual data directly into the pipeline using the -[Helium web interface](https://github.com/mattbdean/Helium). -The raw data are preprocessed using the DataJoint client libraries in MATLAB and Python; -the preprocessed data are ingested into the pipeline while the bulky and raw data are -shared using [Globus](https://globus.org) transfer through the -[PETREL](https://www.alcf.anl.gov/petrel) storage servers provided by the Argonne -National Lab. -Data are made immediately available for exploration and analysis to collaborating labs, -and the analysis results are also immediately shared. -Analysis data may be visualized through web interfaces. -Intermediate results may be exported into the [NWB](https://nwb.org) format for sharing -with external groups. - -## Summary of DataJoint features - -1. A free, open-source framework for scientific data pipelines and workflow management -2. Data hosting in cloud or in-house -3. MySQL, filesystems, S3, and Globus for data management -4. Define, visualize, and query data pipelines from MATLAB or Python -5. Enter and view data through GUIs -6. Concurrent access by multiple users and computational agents -7. Data integrity: identification, dependencies, groupings -8. Automated distributed computation diff --git a/docs/src/archive/concepts/principles.md b/docs/src/archive/concepts/principles.md deleted file mode 100644 index 2bf491590..000000000 --- a/docs/src/archive/concepts/principles.md +++ /dev/null @@ -1,136 +0,0 @@ -# Principles - -## Theoretical Foundations - -*DataJoint Core* implements a systematic framework for the joint management of -structured scientific data and its associated computations. -The framework builds on the theoretical foundations of the -[Relational Model](https://en.wikipedia.org/wiki/Relational_model) and -the [Entity-Relationship Model](https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model), -introducing a number of critical clarifications for the effective use of databases as -scientific data pipelines. -Notably, DataJoint introduces the concept of *computational dependencies* as a native -first-class citizen of the data model. -This integration of data structure and computation into a single model, defines a new -class of *computational scientific databases*. - -This page defines the key principles of this model without attachment to a specific -implementation while a more complete description of the model can be found in -[Yatsenko et al, 2018](https://doi.org/10.48550/arXiv.1807.11104). - -DataJoint developers are developing these principles into an -[open standard](https://en.wikipedia.org/wiki/Open_standard) to allow multiple -alternative implementations. - -## Data Representation - -### Tables = Entity Sets - -DataJoint uses only one data structure in all its operations—the *entity set*. - -1. All data are represented in the form of *entity sets*, i.e. an ordered collection of -*entities*. -2. All entities of an entity set belong to the same well-defined entity class and have -the same set of named attributes. -3. Attributes in an entity set has a *data type* (or *domain*), representing the set of -its valid values. -4. Each entity in an entity set provides the *attribute values* for all of the -attributes of its entity class. -5. Each entity set has a *primary key*, *i.e.* a subset of attributes that, jointly, -uniquely identify any entity in the set. - -These formal terms have more common (even if less precise) variants: - -| formal | common | -|:-:|:--:| -| entity set | *table* | -| attribute | *column* | -| attribute value | *field* | - -A collection of *stored tables* make up a *database*. -*Derived tables* are formed through *query expressions*. - -### Table Definition - -DataJoint introduces a streamlined syntax for defining a stored table. - -Each line in the definition defines an attribute with its name, data type, an optional -default value, and an optional comment in the format: - -```python -name [=default] : type [# comment] -``` - -Primary attributes come first and are separated from the rest of the attributes with -the divider `---`. - -For example, the following code defines the entity set for entities of class `Employee`: - -```python -employee_id : int ---- -ssn = null : int # optional social security number -date_of_birth : date -gender : enum('male', 'female', 'other') -home_address="" : varchar(1000) -primary_phone="" : varchar(12) -``` - -### Data Tiers - -Stored tables are designated into one of four *tiers* indicating how their data -originates. - -| table tier | data origin | -| --- | --- | -| lookup | contents are part of the table definition, defined *a priori* rather than entered externally. Typical stores general facts, parameters, options, *etc.* | -| manual | contents are populated by external mechanisms such as manual entry through web apps or by data ingest scripts | -| imported | contents are populated automatically by pipeline computations accessing data from upstream in the pipeline **and** from external data sources such as raw data stores.| -| computed | contents are populated automatically by pipeline computations accessing data from upstream in the pipeline. | - -### Object Serialization - -### Data Normalization - -A collection of data is considered normalized when organized into a collection of -entity sets, where each entity set represents a well-defined entity class with all its -attributes applicable to each entity in the set and the same primary key identifying - -The normalization procedure often includes splitting data from one table into several -tables, one for each proper entity set. - -### Databases and Schemas - -Stored tables are named and grouped into namespaces called *schemas*. -A collection of schemas make up a *database*. -A *database* has a globally unique address or name. -A *schema* has a unique name within its database. -Within a *connection* to a particular database, a stored table is identified as -`schema.Table`. -A schema typically groups tables that are logically related. - -## Dependencies - -Entity sets can form referential dependencies that express and - -### Diagramming - -## Data integrity - -### Entity integrity - -*Entity integrity* is the guarantee made by the data management process of the 1:1 -mapping between real-world entities and their digital representations. -In practice, entity integrity is ensured when it is made clear - -### Referential integrity - -### Group integrity - -## Data manipulations - -## Data queries - -### Query Operators - -## Pipeline computations diff --git a/docs/src/archive/concepts/teamwork.md b/docs/src/archive/concepts/teamwork.md deleted file mode 100644 index a0a782dde..000000000 --- a/docs/src/archive/concepts/teamwork.md +++ /dev/null @@ -1,97 +0,0 @@ -# Teamwork - -## Data management in a science project - -Science labs organize their projects as a sequence of activities of experiment design, -data acquisition, and processing and analysis. - -![data science in a science lab](../images/data-science-before.png){: style="width:510px; display:block; margin: 0 auto;"} - -
Workflow and dataflow in a common findings-centered approach to data science in a science lab.
- -Many labs lack a uniform data management strategy that would span longitudinally across -the entire project lifecycle as well as laterally across different projects. - -Prior to publishing their findings, the research team may need to publish the data to -support their findings. -Without a data management system, this requires custom repackaging of the data to -conform to the [FAIR principles](https://www.nature.com/articles/sdata201618) for -scientific data management. - -## Data-centric project organization - -DataJoint is designed to support a data-centric approach to large science projects in -which data are viewed as a principal output of the research project and are managed -systematically throughout in a single framework through the entire process. - -This approach requires formulating a general data science plan and upfront investment -for setting up resources and processes and training the teams. -The team uses DataJoint to build data pipelines to support multiple projects. - -![data science in a science lab](../images/data-science-after.png){: style="width:510px; display:block; margin: 0 auto;"} - -
Workflow and dataflow in a data pipeline-centered approach.
- -Data pipelines support project data across their entire lifecycle, including the -following functions - -- experiment design -- animal colony management -- electronic lab book: manual data entry during experiments through graphical user interfaces. -- acquisition from instrumentation in the course of experiments -- ingest from raw acquired data -- computations for data analysis -- visualization of analysis results -- export for sharing and publishing - -Through all these activities, all these data are made accessible to all authorized -participants and distributed computations can be done in parallel without compromising -data integrity. - -## Team roles - -The adoption of a uniform data management framework allows separation of roles and -division of labor among team members, leading to greater efficiency and better scaling. - -![data science in a science lab](../images/data-engineering.png){: style="width:510px; display:block; margin: 0 auto;"} - -
Distinct responsibilities of data science and data engineering.
- -### Scientists - -Design and conduct experiments, collecting data. -They interact with the data pipeline through graphical user interfaces designed by -others. -They understand what analysis is used to test their hypotheses. - -### Data scientists - -Have the domain expertise and select and implement the processing and analysis -methods for experimental data. -Data scientists are in charge of defining and managing the data pipeline using -DataJoint's data model, but they may not know the details of the underlying -architecture. -They interact with the pipeline using client programming interfaces directly from -languages such as MATLAB and Python. - -The bulk of this manual is written for working data scientists, except for System -Administration. - -### Data engineers - -Work with the data scientists to support the data pipeline. -They rely on their understanding of the DataJoint data model to configure and -administer the required IT resources such as database servers, data storage -servers, networks, cloud instances, [Globus](https://globus.org) endpoints, etc. -Data engineers can provide general solutions such as web hosting, data publishing, -interfaces, exports and imports. - -The System Administration section of this tutorial contains materials helpful in -accomplishing these tasks. - -DataJoint is designed to delineate a clean boundary between **data science** and **data -engineering**. -This allows data scientists to use the same uniform data model for data pipelines -backed by a variety of information technologies. -This delineation also enables economies of scale as a single data engineering team can -support a wide spectrum of science projects. diff --git a/docs/src/archive/concepts/terminology.md b/docs/src/archive/concepts/terminology.md deleted file mode 100644 index 0fdc41e96..000000000 --- a/docs/src/archive/concepts/terminology.md +++ /dev/null @@ -1,127 +0,0 @@ - - -# Terminology - -DataJoint introduces a principled data model, which is described in detail in -[Yatsenko et al., 2018](https://arxiv.org/abs/1807.11104). -This data model is a conceptual refinement of the Relational Data Model and also draws -on the Entity-Relationship Model (ERM). - -The Relational Data Model was inspired by the concepts of relations in Set Theory. -When the formal relational data model was formulated, it introduced additional -terminology (e.g. *relation*, *attribute*, *tuple*, *domain*). -Practical programming languages such as SQL do not precisely follow the relational data -model and introduce other terms to approximate relational concepts (e.g. *table*, -*column*, *row*, *datatype*). -Subsequent data models (e.g. ERM) refined the relational data model and introduced -their own terminology to describe analogous concepts (e.g. *entity set*, -*relationship set*, *attribute set*). -As a result, similar concepts may be described using different sets of terminologies, -depending on the context and the speaker's background. - -For example, what is known as a **relation** in the formal relational model is called a -**table** in SQL; the analogous concept in ERM and DataJoint is called an **entity -set**. - -The DataJoint documentation follows the terminology defined in -[Yatsenko et al, 2018](https://arxiv.org/abs/1807.11104), except *entity set* is -replaced with the more colloquial *table* or *query result* in most cases. - -The table below summarizes the terms used for similar concepts across the related data -models. - -Data model terminology -| Relational | ERM | SQL | DataJoint (formal) | This manual | -| -- | -- | -- | -- | -- | -| relation | entity set | table | entity set | table | -| tuple | entity | row | entity | entity | -| domain | value set | datatype | datatype | datatype | -| attribute | attribute | column | attribute | attribute | -| attribute value | attribute value | field value | attribute value | attribute value | -| primary key | primary key | primary key | primary key | primary key | -| foreign key | foreign key | foreign key | foreign key | foreign key | -| schema | schema | schema or database | schema | schema | -| relational expression | data query | `SELECT` statement | query expression | query expression | - -## DataJoint: databases, schemas, packages, and modules - -A **database** is collection of tables on the database server. -DataJoint users do not interact with it directly. - -A **DataJoint schema** is - - - a database on the database server containing tables with data *and* - - a collection of classes (in MATLAB or Python) associated with the database, one - class for each table. - -In MATLAB, the collection of classes is organized as a **package**, i.e. a file folder -starting with a `+`. - -In Python, the collection of classes is any set of classes decorated with the -appropriate `schema` object. -Very commonly classes for tables in one database are organized as a distinct Python -module. -Thus, typical DataJoint projects have one module per database. -However, this organization is up to the user's discretion. - -## Base tables - -**Base tables** are tables stored in the database, and are often referred to simply as -*tables* in DataJoint. -Base tables are distinguished from **derived tables**, which result from relational -[operators](../query/operators.md). - -## Relvars and relation values - -Early versions of the DataJoint documentation referred to the relation objects as -[relvars](https://en.wikipedia.org/wiki/Relvar). -This term emphasizes the fact that relational variables and expressions do not contain -actual data but are rather symbolic representations of data to be retrieved from the -database. -The specific value of a relvar would then be referred to as the **relation value**. -The value of a relvar can change with changes in the state of the database. - -The more recent iteration of the documentation has grown less pedantic and more often -uses the term *table* instead. - -## Metadata - -The vocabulary of DataJoint does not include this term. - -In data science, the term **metadata** commonly means "data about the data" rather than -the data themselves. -For example, metadata could include data sizes, timestamps, data types, indexes, -keywords. - -In contrast, neuroscientists often use the term to refer to conditions and annotations -about experiments. -This distinction arose when such information was stored separately from experimental -recordings, such as in physical notebooks. -Such "metadata" are used to search and to classify the data and are in fact an integral -part of the *actual* data. - -In DataJoint, all data other than blobs can be used in searches and categorization. -These fields may originate from manual annotations, preprocessing, or analyses just as -easily as from recordings or behavioral performance. -Since "metadata" in the neuroscience sense are not distinguished from any other data in -a pipeline, DataJoint avoids the term entirely. -Instead, DataJoint differentiates data into [data tiers](../design/tables/tiers.md). - -## Glossary - -We've taken careful consideration to use consistent terminology. - - - -| Term | Definition | -| --- | --- | -| DAG | directed acyclic graph (DAG) is a set of nodes and connected with a set of directed edges that form no cycles. This means that there is never a path back to a node after passing through it by following the directed edges. Formal workflow management systems represent workflows in the form of DAGs. | -| data pipeline | A sequence of data transformation steps from data sources through multiple intermediate structures. More generally, a data pipeline is a directed acyclic graph. In DataJoint, each step is represented by a table in a relational database. | -| DataJoint | a software framework for database programming directly from matlab and python. Thanks to its support of automated computational dependencies, DataJoint serves as a workflow management system. | -| DataJoint Elements | software modules implementing portions of experiment workflows designed for ease of integration into diverse custom workflows. | -| DataJoint pipeline | the data schemas and transformations underlying a DataJoint workflow. DataJoint allows defining code that specifies both the workflow and the data pipeline, and we have used the words "pipeline" and "workflow" almost interchangeably. | -| DataJoint schema | a software module implementing a portion of an experiment workflow. Includes database table definitions, dependencies, and associated computations. | -| foreign key | a field that is linked to another table's primary key. | -| primary key | the subset of table attributes that uniquely identify each entity in the table. | -| secondray attribute | any field in a table not in the primary key. | -| workflow | a formal representation of the steps for executing an experiment from data collection to analysis. Also the software configured for performing these steps. A typical workflow is composed of tables with inter-dependencies and processes to compute and insert data into the tables. | diff --git a/docs/src/archive/design/alter.md b/docs/src/archive/design/alter.md deleted file mode 100644 index 70ed39341..000000000 --- a/docs/src/archive/design/alter.md +++ /dev/null @@ -1,53 +0,0 @@ -# Altering Populated Pipelines - -Tables can be altered after they have been declared and populated. This is useful when -you want to add new secondary attributes or change the data type of existing attributes. -Users can use the `definition` property to update a table's attributes and then use -`alter` to apply the changes in the database. Currently, `alter` does not support -changes to primary key attributes. - -Let's say we have a table `Student` with the following attributes: - -```python -@schema -class Student(dj.Manual): - definition = """ - student_id: int - --- - first_name: varchar(40) - last_name: varchar(40) - home_address: varchar(100) - """ -``` - -We can modify the table to include a new attribute `email`: - -```python -Student.definition = """ -student_id: int ---- -first_name: varchar(40) -last_name: varchar(40) -home_address: varchar(100) -email: varchar(100) -""" -Student.alter() -``` - -The `alter` method will update the table in the database to include the new attribute -`email` added by the user in the table's `definition` property. - -Similarly, you can modify the data type or length of an existing attribute. For example, -to alter the `home_address` attribute to have a length of 200 characters: - -```python -Student.definition = """ -student_id: int ---- -first_name: varchar(40) -last_name: varchar(40) -home_address: varchar(200) -email: varchar(100) -""" -Student.alter() -``` diff --git a/docs/src/archive/design/diagrams.md b/docs/src/archive/design/diagrams.md deleted file mode 100644 index 826f78926..000000000 --- a/docs/src/archive/design/diagrams.md +++ /dev/null @@ -1,110 +0,0 @@ -# Diagrams - -Diagrams are a great way to visualize the pipeline and understand the flow -of data. DataJoint diagrams are based on **entity relationship diagram** (ERD). -Objects of type `dj.Diagram` allow visualizing portions of the data pipeline in -graphical form. -Tables are depicted as nodes and [dependencies](./tables/dependencies.md) as directed -edges between them. -The `draw` method plots the graph. - -## Diagram notation - -Consider the following diagram - -![mp-diagram](../images/mp-diagram.png){: style="align:center"} - -DataJoint uses the following conventions: - -- Tables are indicated as nodes in the graph. - The corresponding class name is indicated by each node. -- [Data tiers](./tables/tiers.md) are indicated as colors and symbols: - - Lookup=gray rectangle - - Manual=green rectangle - - Imported=blue oval - - Computed=red circle - - Part=black text - The names of [part tables](./tables/master-part.md) are indicated in a smaller font. -- [Dependencies](./tables/dependencies.md) are indicated as edges in the graph and -always directed downward, forming a **directed acyclic graph**. -- Foreign keys contained within the primary key are indicated as solid lines. - This means that the referenced table becomes part of the primary key of the dependent table. -- Foreign keys that are outside the primary key are indicated by dashed lines. -- If the primary key of the dependent table has no other attributes besides the foreign -key, the foreign key is a thick solid line, indicating a 1:{0,1} relationship. -- Foreign keys made without renaming the foreign key attributes are in black whereas -foreign keys that rename the attributes are indicated in red. - -## Diagramming an entire schema - -To plot the Diagram for an entire schema, an Diagram object can be initialized with the -schema object (which is normally used to decorate table objects) - -```python -import datajoint as dj -schema = dj.Schema('my_database') -dj.Diagram(schema).draw() -``` - -or alternatively an object that has the schema object as an attribute, such as the -module defining a schema: - -```python -import datajoint as dj -import seq # import the sequence module defining the seq database -dj.Diagram(seq).draw() # draw the Diagram -``` - -Note that calling the `.draw()` method is not necessary when working in a Jupyter -notebook. -You can simply let the object display itself, for example by entering `dj.Diagram(seq)` -in a notebook cell. -The Diagram will automatically render in the notebook by calling its `_repr_html_` -method. -A Diagram displayed without `.draw()` will be rendered as an SVG, and hovering the -mouse over a table will reveal a compact version of the output of the `.describe()` -method. - -### Initializing with a single table - -A `dj.Diagram` object can be initialized with a single table. - -```python -dj.Diagram(seq.Genome).draw() -``` - -A single node makes a rather boring graph but ERDs can be added together or subtracted -from each other using graph algebra. - -### Adding diagrams together - -However two graphs can be added, resulting in new graph containing the union of the -sets of nodes from the two original graphs. -The corresponding foreign keys will be automatically - -```python -# plot the Diagram with tables Genome and Species from module seq. -(dj.Diagram(seq.Genome) + dj.Diagram(seq.Species)).draw() -``` - -### Expanding diagrams upstream and downstream - -Adding a number to an Diagram object adds nodes downstream in the pipeline while -subtracting a number from Diagram object adds nodes upstream in the pipeline. - -Examples: - -```python -# Plot all the tables directly downstream from `seq.Genome` -(dj.Diagram(seq.Genome)+1).draw() -``` - -```python -# Plot all the tables directly upstream from `seq.Genome` -(dj.Diagram(seq.Genome)-1).draw() -``` - -```python -# Plot the local neighborhood of `seq.Genome` -(dj.Diagram(seq.Genome)+1-1+1-1).draw() -``` diff --git a/docs/src/archive/design/drop.md b/docs/src/archive/design/drop.md deleted file mode 100644 index 35a9ac513..000000000 --- a/docs/src/archive/design/drop.md +++ /dev/null @@ -1,23 +0,0 @@ -# Drop - -The `drop` method completely removes a table from the database, including its -definition. -It also removes all dependent tables, recursively. -DataJoint will first display the tables being dropped and the number of entities in -each before prompting the user for confirmation to proceed. - -The `drop` method is often used during initial design to allow altered table -definitions to take effect. - -```python -# drop the Person table from its schema -Person.drop() -``` - -## Dropping part tables - -A [part table](../design/tables/master-part.md) is usually removed as a consequence of -calling `drop` on its master table. -To enforce this workflow, calling `drop` directly on a part table produces an error. -In some cases, it may be necessary to override this behavior. -To remove a part table without removing its master, use the argument `force=True`. diff --git a/docs/src/archive/design/fetch-api-2.0-spec.md b/docs/src/archive/design/fetch-api-2.0-spec.md deleted file mode 100644 index a996a5f08..000000000 --- a/docs/src/archive/design/fetch-api-2.0-spec.md +++ /dev/null @@ -1,302 +0,0 @@ -# DataJoint 2.0 Fetch API Specification - -## Overview - -DataJoint 2.0 replaces the complex `fetch()` method with a set of explicit, composable output methods. This provides better discoverability, clearer intent, and more efficient iteration. - -## Design Principles - -1. **Explicit over implicit**: Each output format has its own method -2. **Composable**: Use existing `.proj()` for column selection -3. **Lazy iteration**: Single cursor streaming instead of fetch-all-keys -4. **Modern formats**: First-class support for polars and Arrow - ---- - -## New API Reference - -### Output Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `to_dicts()` | `list[dict]` | All rows as list of dictionaries | -| `to_pandas()` | `DataFrame` | pandas DataFrame with primary key as index | -| `to_polars()` | `polars.DataFrame` | polars DataFrame (requires `datajoint[polars]`) | -| `to_arrow()` | `pyarrow.Table` | PyArrow Table (requires `datajoint[arrow]`) | -| `to_arrays()` | `np.ndarray` | numpy structured array (recarray) | -| `to_arrays('a', 'b')` | `tuple[array, array]` | Tuple of arrays for specific columns | -| `keys()` | `list[dict]` | Primary key values only | -| `fetch1()` | `dict` | Single row as dict (raises if not exactly 1) | -| `fetch1('a', 'b')` | `tuple` | Single row attribute values | - -### Common Parameters - -All output methods accept these optional parameters: - -```python -table.to_dicts( - order_by=None, # str or list: column(s) to sort by, e.g. "KEY", "name DESC" - limit=None, # int: maximum rows to return - offset=None, # int: rows to skip - squeeze=False, # bool: remove singleton dimensions from arrays - download_path="." # str: path for downloading external data -) -``` - -### Iteration - -```python -# Lazy streaming - yields one dict per row from database cursor -for row in table: - process(row) # row is a dict -``` - ---- - -## Migration Guide - -### Basic Fetch Operations - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `table.fetch()` | `table.to_arrays()` or `table.to_dicts()` | -| `table.fetch(format="array")` | `table.to_arrays()` | -| `table.fetch(format="frame")` | `table.to_pandas()` | -| `table.fetch(as_dict=True)` | `table.to_dicts()` | - -### Attribute Fetching - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `table.fetch('a')` | `table.to_arrays('a')` | -| `a, b = table.fetch('a', 'b')` | `a, b = table.to_arrays('a', 'b')` | -| `table.fetch('a', 'b', as_dict=True)` | `table.proj('a', 'b').to_dicts()` | - -### Primary Key Fetching - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `table.fetch('KEY')` | `table.keys()` | -| `table.fetch(dj.key)` | `table.keys()` | -| `keys, a = table.fetch('KEY', 'a')` | See note below | - -For mixed KEY + attribute fetch: -```python -# Old: keys, a = table.fetch('KEY', 'a') -# New: Combine keys() with to_arrays() -keys = table.keys() -a = table.to_arrays('a') -# Or use to_dicts() which includes all columns -``` - -### Ordering, Limiting, Offset - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `table.fetch(order_by='name')` | `table.to_arrays(order_by='name')` | -| `table.fetch(limit=10)` | `table.to_arrays(limit=10)` | -| `table.fetch(order_by='KEY', limit=10, offset=5)` | `table.to_arrays(order_by='KEY', limit=10, offset=5)` | - -### Single Row Fetch (fetch1) - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `table.fetch1()` | `table.fetch1()` (unchanged) | -| `a, b = table.fetch1('a', 'b')` | `a, b = table.fetch1('a', 'b')` (unchanged) | -| `table.fetch1('KEY')` | `table.fetch1()` then extract pk columns | - -### Configuration - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `dj.config['fetch_format'] = 'frame'` | Use `.to_pandas()` explicitly | -| `with dj.config.override(fetch_format='frame'):` | Use `.to_pandas()` in the block | - -### Iteration - -| Old Pattern (1.x) | New Pattern (2.0) | -|-------------------|-------------------| -| `for row in table:` | `for row in table:` (same syntax, now lazy!) | -| `list(table)` | `table.to_dicts()` | - -### Column Selection with proj() - -Use `.proj()` for column selection, then apply output method: - -```python -# Select specific columns -table.proj('col1', 'col2').to_pandas() -table.proj('col1', 'col2').to_dicts() - -# Computed columns -table.proj(total='price * quantity').to_pandas() -``` - ---- - -## Removed Features - -### Removed Methods and Parameters - -- `fetch()` method - use explicit output methods -- `fetch('KEY')` - use `keys()` -- `dj.key` class - use `keys()` method -- `format=` parameter - use explicit methods -- `as_dict=` parameter - use `to_dicts()` -- `config['fetch_format']` setting - use explicit methods - -### Removed Imports - -```python -# Old (removed) -from datajoint import key -result = table.fetch(dj.key) - -# New -result = table.keys() -``` - ---- - -## Examples - -### Example 1: Basic Data Retrieval - -```python -# Get all data as DataFrame -df = Experiment().to_pandas() - -# Get all data as list of dicts -rows = Experiment().to_dicts() - -# Get all data as numpy array -arr = Experiment().to_arrays() -``` - -### Example 2: Filtered and Sorted Query - -```python -# Get recent experiments, sorted by date -recent = (Experiment() & 'date > "2024-01-01"').to_pandas( - order_by='date DESC', - limit=100 -) -``` - -### Example 3: Specific Columns - -```python -# Fetch specific columns as arrays -names, dates = Experiment().to_arrays('name', 'date') - -# Or with primary key included -names, dates = Experiment().to_arrays('name', 'date', include_key=True) -``` - -### Example 4: Primary Keys for Iteration - -```python -# Get keys for restriction -keys = Experiment().keys() -for key in keys: - process(Session() & key) -``` - -### Example 5: Single Row - -```python -# Get one row as dict -row = (Experiment() & key).fetch1() - -# Get specific attributes -name, date = (Experiment() & key).fetch1('name', 'date') -``` - -### Example 6: Lazy Iteration - -```python -# Stream rows efficiently (single database cursor) -for row in Experiment(): - if should_process(row): - process(row) - if done: - break # Early termination - no wasted fetches -``` - -### Example 7: Modern DataFrame Libraries - -```python -# Polars (fast, modern) -import polars as pl -df = Experiment().to_polars() -result = df.filter(pl.col('value') > 100).group_by('category').agg(pl.mean('value')) - -# PyArrow (zero-copy interop) -table = Experiment().to_arrow() -# Can convert to pandas or polars with zero copy -``` - ---- - -## Performance Considerations - -### Lazy Iteration - -The new iteration is significantly more efficient: - -```python -# Old (1.x): N+1 queries -# 1. fetch("KEY") gets ALL keys -# 2. fetch1() for EACH key - -# New (2.0): Single query -# Streams rows from one cursor -for row in table: - ... -``` - -### Memory Efficiency - -- `to_dicts()`: Returns full list in memory -- `for row in table:`: Streams one row at a time -- `to_arrays(limit=N)`: Fetches only N rows - -### Format Selection - -| Use Case | Recommended Method | -|----------|-------------------| -| Data analysis | `to_pandas()` or `to_polars()` | -| JSON API responses | `to_dicts()` | -| Numeric computation | `to_arrays()` | -| Large datasets | `for row in table:` (streaming) | -| Interop with other tools | `to_arrow()` | - ---- - -## Error Messages - -When attempting to use removed methods, users see helpful error messages: - -```python ->>> table.fetch() -AttributeError: fetch() has been removed in DataJoint 2.0. -Use to_dicts(), to_pandas(), to_arrays(), or keys() instead. -See table.fetch.__doc__ for details. -``` - ---- - -## Optional Dependencies - -Install optional dependencies for additional output formats: - -```bash -# For polars support -pip install datajoint[polars] - -# For PyArrow support -pip install datajoint[arrow] - -# For both -pip install datajoint[polars,arrow] -``` diff --git a/docs/src/archive/design/hidden-job-metadata-spec.md b/docs/src/archive/design/hidden-job-metadata-spec.md deleted file mode 100644 index a33a8d51d..000000000 --- a/docs/src/archive/design/hidden-job-metadata-spec.md +++ /dev/null @@ -1,355 +0,0 @@ -# Hidden Job Metadata in Computed Tables - -## Overview - -Job execution metadata (start time, duration, code version) should be persisted in computed tables themselves, not just in ephemeral job entries. This is accomplished using hidden attributes. - -## Motivation - -The current job table (`~~table_name`) tracks execution metadata, but: -1. Job entries are deleted after completion (unless `keep_completed=True`) -2. Users often need to know when and with what code version each row was computed -3. This metadata should be transparent - not cluttering the user-facing schema - -Hidden attributes (prefixed with `_`) provide the solution: stored in the database but filtered from user-facing APIs. - -## Hidden Job Metadata Attributes - -| Attribute | Type | Description | -|-----------|------|-------------| -| `_job_start_time` | datetime(3) | When computation began | -| `_job_duration` | float32 | Computation duration in seconds | -| `_job_version` | varchar(64) | Code version (e.g., git commit hash) | - -**Design notes:** -- `_job_duration` (elapsed time) rather than `_job_completed_time` because duration is more informative for performance analysis -- `varchar(64)` for version is sufficient for git hashes (40 chars for SHA-1, 7-8 for short hash) -- `datetime(3)` provides millisecond precision - -## Configuration - -### Settings Structure - -Job metadata is controlled via `config.jobs` settings: - -```python -class JobsSettings(BaseSettings): - """Job queue configuration for AutoPopulate 2.0.""" - - model_config = SettingsConfigDict( - env_prefix="DJ_JOBS_", - case_sensitive=False, - extra="forbid", - validate_assignment=True, - ) - - # Existing settings - auto_refresh: bool = Field(default=True, ...) - keep_completed: bool = Field(default=False, ...) - stale_timeout: int = Field(default=3600, ...) - default_priority: int = Field(default=5, ...) - version_method: Literal["git", "none"] | None = Field(default=None, ...) - allow_new_pk_fields_in_computed_tables: bool = Field(default=False, ...) - - # New setting for hidden job metadata - add_job_metadata: bool = Field( - default=False, - description="Add hidden job metadata attributes (_job_start_time, _job_duration, _job_version) " - "to Computed and Imported tables during declaration. Tables created without this setting " - "will not receive metadata updates during populate." - ) -``` - -### Access Patterns - -```python -import datajoint as dj - -# Read setting -dj.config.jobs.add_job_metadata # False (default) - -# Enable programmatically -dj.config.jobs.add_job_metadata = True - -# Enable via environment variable -# DJ_JOBS_ADD_JOB_METADATA=true - -# Enable in config file (dj_config.yaml) -# jobs: -# add_job_metadata: true - -# Temporary override -with dj.config.override(jobs={"add_job_metadata": True}): - schema(MyComputedTable) # Declared with metadata columns -``` - -### Setting Interactions - -| Setting | Effect on Job Metadata | -|---------|----------------------| -| `add_job_metadata=True` | New Computed/Imported tables get hidden metadata columns | -| `add_job_metadata=False` | Tables declared without metadata columns (default) | -| `version_method="git"` | `_job_version` populated with git short hash | -| `version_method="none"` | `_job_version` left empty | -| `version_method=None` | `_job_version` left empty (same as "none") | - -### Behavior at Declaration vs Populate - -| `add_job_metadata` at declare | `add_job_metadata` at populate | Result | -|------------------------------|-------------------------------|--------| -| True | True | Metadata columns created and populated | -| True | False | Metadata columns exist but not populated | -| False | True | No metadata columns, populate skips silently | -| False | False | No metadata columns, normal behavior | - -### Retrofitting Existing Tables - -Tables created before enabling `add_job_metadata` do not have the hidden metadata columns. -To add metadata columns to existing tables, use the migration utility (not automatic): - -```python -from datajoint.migrate import add_job_metadata_columns - -# Add hidden metadata columns to specific table -add_job_metadata_columns(MyComputedTable) - -# Add to all Computed/Imported tables in a schema -add_job_metadata_columns(schema) -``` - -This utility: -- ALTERs the table to add the three hidden columns -- Does NOT populate existing rows (metadata remains NULL) -- Future `populate()` calls will populate metadata for new rows - -## Behavior - -### Declaration-time - -When `config.jobs.add_job_metadata=True` and a Computed/Imported table is declared: -- Hidden metadata columns are added to the table definition -- Only master tables receive metadata columns; Part tables never get them - -### Population-time - -After `make()` completes successfully: -1. Check if the table has hidden metadata columns -2. If yes: UPDATE the just-inserted rows with start_time, duration, version -3. If no: Silently skip (no error, no ALTER) - -This applies to both: -- **Direct mode** (`reserve_jobs=False`): Single-process populate -- **Distributed mode** (`reserve_jobs=True`): Multi-worker with job table coordination - -## Excluding Hidden Attributes from Binary Operators - -### Problem Statement - -If two tables have hidden attributes with the same name (e.g., both have `_job_start_time`), SQL's NATURAL JOIN would incorrectly match on them: - -```sql --- NATURAL JOIN matches ALL common attributes including hidden -SELECT * FROM table_a NATURAL JOIN table_b --- Would incorrectly match on _job_start_time! -``` - -### Solution: Replace NATURAL JOIN with USING Clause - -Hidden attributes must be excluded from all binary operator considerations. The result of a join does not preserve hidden attributes from its operands. - -**Current implementation:** -```python -def from_clause(self): - clause = next(support) - for s, left in zip(support, self._left): - clause += " NATURAL{left} JOIN {clause}".format(...) -``` - -**Proposed implementation:** -```python -def from_clause(self): - clause = next(support) - for s, (left, using_attrs) in zip(support, self._joins): - if using_attrs: - using = "USING ({})".format(", ".join(f"`{a}`" for a in using_attrs)) - clause += " {left}JOIN {s} {using}".format( - left="LEFT " if left else "", - s=s, - using=using - ) - else: - # Cross join (no common non-hidden attributes) - clause += " CROSS JOIN " + s if not left else " LEFT JOIN " + s + " ON TRUE" - return clause -``` - -### Changes Required - -#### 1. `QueryExpression._left` → `QueryExpression._joins` - -Replace `_left: List[bool]` with `_joins: List[Tuple[bool, List[str]]]` - -Each join stores: -- `left`: Whether it's a left join -- `using_attrs`: Non-hidden common attributes to join on - -```python -# Before -result._left = self._left + [left] + other._left - -# After -join_attributes = [n for n in self.heading.names if n in other.heading.names] -result._joins = self._joins + [(left, join_attributes)] + other._joins -``` - -#### 2. `heading.names` (existing behavior) - -Already filters out hidden attributes: -```python -@property -def names(self): - return [k for k in self.attributes] # attributes excludes is_hidden=True -``` - -This ensures join attribute computation automatically excludes hidden attributes. - -### Behavior Summary - -| Scenario | Hidden Attributes | Result | -|----------|-------------------|--------| -| `A * B` (join) | Same hidden attr in both | NOT matched - excluded from USING | -| `A & B` (semijoin) | Same hidden attr in both | NOT matched | -| `A - B` (antijoin) | Same hidden attr in both | NOT matched | -| `A.proj()` | Hidden attrs in A | NOT projected (unless explicitly named) | -| `A.fetch()` | Hidden attrs in A | NOT returned by default | - -## Implementation Details - -### 1. Declaration (declare.py) - -```python -def declare(full_table_name, definition, context): - # ... existing code ... - - # Add hidden job metadata for auto-populated tables - if config.jobs.add_job_metadata and table_tier in (TableTier.COMPUTED, TableTier.IMPORTED): - # Only for master tables, not parts - if not is_part_table: - job_metadata_sql = [ - "`_job_start_time` datetime(3) DEFAULT NULL", - "`_job_duration` float DEFAULT NULL", - "`_job_version` varchar(64) DEFAULT ''", - ] - attribute_sql.extend(job_metadata_sql) -``` - -### 2. Population (autopopulate.py) - -```python -def _populate1(self, key, callback, use_jobs, jobs): - start_time = datetime.now() - version = _get_job_version() - - # ... call make() ... - - duration = time.time() - start_time.timestamp() - - # Update job metadata if table has the hidden attributes - if self._has_job_metadata_attrs(): - self._update_job_metadata( - key, - start_time=start_time, - duration=duration, - version=version - ) - -def _has_job_metadata_attrs(self): - """Check if table has hidden job metadata columns.""" - hidden_attrs = self.heading._attributes # includes hidden - return '_job_start_time' in hidden_attrs - -def _update_job_metadata(self, key, start_time, duration, version): - """Update hidden job metadata for the given key.""" - # UPDATE using primary key - pk_condition = make_condition(self, key, set()) - self.connection.query( - f"UPDATE {self.full_table_name} SET " - f"`_job_start_time`=%s, `_job_duration`=%s, `_job_version`=%s " - f"WHERE {pk_condition}", - args=(start_time, duration, version[:64]) - ) -``` - -### 3. Job table (jobs.py) - -Update version field length: -```python -version="" : varchar(64) -``` - -### 4. Version helper - -```python -def _get_job_version() -> str: - """Get version string, truncated to 64 chars.""" - from .settings import config - - method = config.jobs.version_method - if method is None or method == "none": - return "" - elif method == "git": - try: - result = subprocess.run( - ["git", "rev-parse", "--short", "HEAD"], - capture_output=True, - text=True, - timeout=5, - ) - return result.stdout.strip()[:64] if result.returncode == 0 else "" - except Exception: - return "" - return "" -``` - -## Example Usage - -```python -# Enable job metadata for new tables -dj.config.jobs.add_job_metadata = True - -@schema -class ProcessedData(dj.Computed): - definition = """ - -> RawData - --- - result : float - """ - - def make(self, key): - # User code - unaware of hidden attributes - self.insert1({**key, 'result': compute(key)}) - -# Job metadata automatically added and populated: -# _job_start_time, _job_duration, _job_version - -# User-facing API unaffected: -ProcessedData().heading.names # ['raw_data_id', 'result'] -ProcessedData().fetch() # Returns only visible attributes - -# Access hidden attributes explicitly if needed: -ProcessedData().fetch('_job_start_time', '_job_duration', '_job_version') -``` - -## Summary of Design Decisions - -| Decision | Resolution | -|----------|------------| -| Configuration | `config.jobs.add_job_metadata` (default False) | -| Environment variable | `DJ_JOBS_ADD_JOB_METADATA` | -| Existing tables | No automatic ALTER - silently skip metadata if columns absent | -| Retrofitting | Manual via `datajoint.migrate.add_job_metadata_columns()` utility | -| Populate modes | Record metadata in both direct and distributed modes | -| Part tables | No metadata columns - only master tables | -| Version length | varchar(64) in both jobs table and computed tables | -| Binary operators | Hidden attributes excluded via USING clause instead of NATURAL JOIN | -| Failed makes | N/A - transaction rolls back, no rows to update | diff --git a/docs/src/archive/design/integrity.md b/docs/src/archive/design/integrity.md deleted file mode 100644 index 393103522..000000000 --- a/docs/src/archive/design/integrity.md +++ /dev/null @@ -1,218 +0,0 @@ -# Data Integrity - -The term **data integrity** describes guarantees made by the data management process -that prevent errors and corruption in data due to technical failures and human errors -arising in the course of continuous use by multiple agents. -DataJoint pipelines respect the following forms of data integrity: **entity -integrity**, **referential integrity**, and **group integrity** as described in more -detail below. - -## Entity integrity - -In a proper relational design, each table represents a collection of discrete -real-world entities of some kind. -**Entity integrity** is the guarantee made by the data management process that entities -from the real world are reliably and uniquely represented in the database system. -Entity integrity states that the data management process must prevent duplicate -representations or misidentification of entities. -DataJoint enforces entity integrity through the use of -[primary keys](./tables/primary.md). - -Entity integrity breaks down when a process allows data pertaining to the same -real-world entity to be entered into the database system multiple times. -For example, a school database system may use unique ID numbers to distinguish students. -Suppose the system automatically generates an ID number each time a student record is -entered into the database without checking whether a record already exists for that -student. -Such a system violates entity integrity, because the same student may be assigned -multiple ID numbers. -The ID numbers succeed in uniquely identifying each student record but fail to do so -for the actual students. - -Note that a database cannot guarantee or enforce entity integrity by itself. -Entity integrity is a property of the entire data management process as a whole, -including institutional practices and user actions in addition to database -configurations. - -## Referential integrity - -**Referential integrity** is the guarantee made by the data management process that -related data across the database remain present, correctly associated, and mutually -consistent. -Guaranteeing referential integrity means enforcing the constraint that no entity can -exist in the database without all the other entities on which it depends. -Referential integrity cannot exist without entity integrity: references to entity -cannot be validated if the identity of the entity itself is not guaranteed. - -Referential integrity fails when a data management process allows new data to be -entered that refers to other data missing from the database. -For example, assume that each electrophysiology recording must refer to the mouse -subject used during data collection. -Perhaps an experimenter attempts to insert ephys data into the database that refers to -a nonexistent mouse, due to a misspelling. -A system guaranteeing referential integrity, such as DataJoint, will refuse the -erroneous data. - -Enforcement of referential integrity does not stop with data ingest. -[Deleting](../manipulation/delete.md) data in DataJoint also deletes any dependent -downstream data. -Such cascading deletions are necessary to maintain referential integrity. -Consider the deletion of a mouse subject without the deletion of the experimental -sessions involving that mouse. -A database that allows such deletion will break referential integrity, as the -experimental sessions for the removed mouse depend on missing data. -Any data management process that allows data to be deleted with no consideration of -dependent data cannot maintain referential integrity. - -[Updating](../manipulation/update.md) data already present in a database system also -jeopardizes referential integrity. -For this reason, the DataJoint workflow does not include updates to entities once they -have been ingested into a pipeline. -Allowing updates to upstream entities would break the referential integrity of any -dependent data downstream. -For example, permitting a user to change the name of a mouse subject would invalidate -any experimental sessions that used that mouse, presuming the mouse name was part of -the primary key. -The proper way to change data in DataJoint is to delete the existing entities and to -insert corrected ones, preserving referential integrity. - -## Group integrity - -**Group integrity** denotes the guarantee made by the data management process that -entities composed of multiple parts always appear in their complete form. -Group integrity in DataJoint is formalized through -[master-part](./tables/master-part.md) relationships. -The master-part relationship has important implications for dependencies, because a -downstream entity depending on a master entity set may be considered to depend on the -parts as well. - -## Relationships - -In DataJoint, the term **relationship** is used rather generally to describe the -effects of particular configurations of [dependencies](./tables/dependencies.md) -between multiple entity sets. -It is often useful to classify relationships as one-to-one, many-to-one, one-to-many, -and many-to-many. - -In a **one-to-one relationship**, each entity in a downstream table has exactly one -corresponding entity in the upstream table. -A dependency of an entity set containing the death dates of mice on an entity set -describing the mice themselves would obviously be a one-to-one relationship, as in the -example below. - -```python -@schema -class Mouse(dj.Manual): -definition = """ -mouse_name : varchar(64) ---- -mouse_dob : datetime -""" - -@schema -class MouseDeath(dj.Manual): -definition = """ --> Mouse ---- -death_date : datetime -""" -``` - -![doc_1-1](../images/doc_1-1.png){: style="align:center"} - -In a **one-to-many relationship**, multiple entities in a downstream table may depend -on the same entity in the upstream table. -The example below shows a table containing individual channel data from multi-channel -recordings, representing a one-to-many relationship. - -```python -@schema -class EEGRecording(dj.Manual): -definition = """ --> Session -eeg_recording_id : int ---- -eeg_system : varchar(64) -num_channels : int -""" - -@schema -class ChannelData(dj.Imported): -definition = """ --> EEGRecording -channel_idx : int ---- -channel_data : -""" -``` -![doc_1-many](../images/doc_1-many.png){: style="align:center"} - -In a **many-to-one relationship**, each entity in a table is associated with multiple -entities from another table. -Many-to-one relationships between two tables are usually established using a separate -membership table. -The example below includes a table of mouse subjects, a table of subject groups, and a -membership [part table](./tables/master-part.md) listing the subjects in each group. -A many-to-one relationship exists between the `Mouse` table and the `SubjectGroup` -table, with is expressed through entities in `GroupMember`. - -```python -@schema -class Mouse(dj.Manual): -definition = """ -mouse_name : varchar(64) ---- -mouse_dob : datetime -""" - -@schema -class SubjectGroup(dj.Manual): -definition = """ -group_number : int ---- -group_name : varchar(64) -""" - -class GroupMember(dj.Part): - definition = """ - -> master - -> Mouse - """ -``` - -![doc_many-1](../images/doc_many-1.png){: style="align:center"} - -In a **many-to-many relationship**, multiple entities in one table may each relate to -multiple entities in another upstream table. -Many-to-many relationships between two tables are usually established using a separate -association table. -Each entity in the association table links one entity from each of the two upstream -tables it depends on. -The below example of a many-to-many relationship contains a table of recording -modalities and a table of multimodal recording sessions. -Entities in a third table represent the modes used for each session. - -```python -@schema -class RecordingModality(dj.Lookup): -definition = """ -modality : varchar(64) -""" - -@schema -class MultimodalSession(dj.Manual): -definition = """ --> Session -modes : int -""" -class SessionMode(dj.Part): - definition = """ - -> master - -> RecordingModality - """ -``` - -![doc_many-many](../images/doc_many-many.png){: style="align:center"} - -The types of relationships between entity sets are expressed in the -[Diagram](diagrams.md) of a schema. diff --git a/docs/src/archive/design/normalization.md b/docs/src/archive/design/normalization.md deleted file mode 100644 index 000028396..000000000 --- a/docs/src/archive/design/normalization.md +++ /dev/null @@ -1,117 +0,0 @@ -# Entity Normalization - -DataJoint uses a uniform way of representing any data. -It does so in the form of **entity sets**, unordered collections of entities of the -same type. -The term **entity normalization** describes the commitment to represent all data as -well-formed entity sets. -Entity normalization is a conceptual refinement of the -[relational data model](../concepts/data-model.md) and is the central principle of the -DataJoint model ([Yatsenko et al., 2018](https://arxiv.org/abs/1807.11104)). -Entity normalization leads to clear and logical database designs and to easily -comprehensible data queries. - -Entity sets are a type of **relation** -(from the [relational data model](../concepts/data-model.md)) and are often visualized -as **tables**. -Hence the terms **relation**, **entity set**, and **table** can be used interchangeably -when entity normalization is assumed. - -## Criteria of a well-formed entity set - -1. All elements of an entity set belong to the same well-defined and readily identified -**entity type** from the model world. -2. All attributes of an entity set are applicable directly to each of its elements, -although some attribute values may be missing (set to null). -3. All elements of an entity set must be distinguishable form each other by the same -primary key. -4. Primary key attribute values cannot be missing, i.e. set to null. -5. All elements of an entity set participate in the same types of relationships with -other entity sets. - -## Entity normalization in schema design - -Entity normalization applies to schema design in that the designer is responsible for -the identification of the essential entity types in their model world and of the -dependencies among the entity types. - -The term entity normalization may also apply to a procedure for refactoring a schema -design that does not meet the above criteria into one that does. -In some cases, this may require breaking up some entity sets into multiple entity sets, -which may cause some entities to be represented across multiple entity sets. -In other cases, this may require converting attributes into their own entity sets. -Technically speaking, entity normalization entails compliance with the -[Boyce-Codd normal form](https://en.wikipedia.org/wiki/Boyce%E2%80%93Codd_normal_form) -while lacking the representational power for the applicability of more complex normal -forms ([Kent, 1983](https://dl.acm.org/citation.cfm?id=358054)). -Adherence to entity normalization prevents redundancies in storage and data -manipulation anomalies. -The same criteria originally motivated the formulation of the classical relational -normal forms. - -## Entity normalization in data queries - -Entity normalization applies to data queries as well. -DataJoint's [query operators](../query/operators.md) are designed to preserve the -entity normalization of their inputs. -For example, the outputs of operators [restriction](../query/restrict.md), -[proj](../query/project.md), and [aggr](../query/aggregation.md) retain the same entity -type as the (first) input. -The [join](../query/join.md) operator produces a new entity type comprising the pairing -of the entity types of its inputs. -[Universal sets](../query/universals.md) explicitly introduce virtual entity sets when -necessary to accomplish a query. - -## Examples of poor normalization - -Design choices lacking entity normalization may lead to data inconsistencies or -anomalies. -Below are several examples of poorly normalized designs and their normalized -alternatives. - -### Indirect attributes - -All attributes should apply to the entity itself. -Avoid attributes that actually apply to one of the entity's other attributes. -For example, consider the table `Author` with attributes `author_name`, `institution`, -and `institution_address`. -The attribute `institution_address` should really be held in a separate `Institution` -table that `Author` depends on. - -### Repeated attributes - -Avoid tables with repeated attributes of the same category. -A better solution is to create a separate table that depends on the first (often a -[part table](../design/tables/master-part.md)), with multiple individual entities -rather than repeated attributes. -For example, consider the table `Protocol` that includes the attributes `equipment1`, -`equipment2`, and `equipment3`. -A better design would be to create a `ProtocolEquipment` table that links each entity -in `Protocol` with multiple entities in `Equipment` through -[dependencies](../design/tables/dependencies.md). - -### Attributes that do not apply to all entities - -All attributes should be relevant to every entity in a table. -Attributes that apply only to a subset of entities in a table likely belong in a -separate table containing only that subset of entities. -For example, a table `Protocol` should include the attribute `stimulus` only if all -experiment protocols include stimulation. -If the not all entities in `Protocol` involve stimulation, then the `stimulus` -attribute should be moved to a part table that has `Protocol` as its master. -Only protocols using stimulation will have an entry in this part table. - -### Transient attributes - -Attributes should be relevant to all entities in a table at all times. -Attributes that do not apply to all entities should be moved to another dependent table -containing only the appropriate entities. -This principle also applies to attributes that have not yet become meaningful for some -entities or that will not remain meaningful indefinitely. -For example, consider the table `Mouse` with attributes `birth_date` and `death_date`, -where `death_date` is set to `NULL` for living mice. -Since the `death_date` attribute is not meaningful for mice that are still living, -the proper design would include a separate table `DeceasedMouse` that depends on -`Mouse`. -`DeceasedMouse` would only contain entities for dead mice, which improves integrity and -averts the need for [updates](../manipulation/update.md). diff --git a/docs/src/archive/design/pk-rules-spec.md b/docs/src/archive/design/pk-rules-spec.md deleted file mode 100644 index c6e2dc8ea..000000000 --- a/docs/src/archive/design/pk-rules-spec.md +++ /dev/null @@ -1,318 +0,0 @@ -# Primary Key Rules in Relational Operators - -In DataJoint, the result of each query operator produces a valid **entity set** with a well-defined **entity type** and **primary key**. This section specifies how the primary key is determined for each relational operator. - -## General Principle - -The primary key of a query result identifies unique entities in that result. For most operators, the primary key is preserved from the left operand. For joins, the primary key depends on the functional dependencies between the operands. - -## Integration with Semantic Matching - -Primary key determination is applied **after** semantic compatibility is verified. The evaluation order is: - -1. **Semantic Check**: `assert_join_compatibility()` ensures all namesakes are homologous (same lineage) -2. **PK Determination**: The "determines" relationship is computed using attribute names -3. **Left Join Validation**: If `left=True`, verify A → B - -This ordering is important because: -- After semantic matching passes, namesakes represent semantically equivalent attributes -- The name-based "determines" check is therefore semantically valid -- Attribute names in the context of a semantically-valid join represent the same entity - -The "determines" relationship uses attribute **names** (not lineages directly) because: -- Lineage ensures namesakes are homologous -- Once verified, checking by name is equivalent to checking by semantic identity -- Aliased attributes (same lineage, different names) don't participate in natural joins anyway - -## Notation - -In the examples below, `*` marks primary key attributes: -- `A(x*, y*, z)` means A has primary key `{x, y}` and secondary attribute `z` -- `A → B` means "A determines B" (defined below) - -### Rules by Operator - -| Operator | Primary Key Rule | -|----------|------------------| -| `A & B` (restriction) | PK(A) — preserved from left operand | -| `A - B` (anti-restriction) | PK(A) — preserved from left operand | -| `A.proj(...)` (projection) | PK(A) — preserved from left operand | -| `A.aggr(B, ...)` (aggregation) | PK(A) — preserved from left operand | -| `A.extend(B)` (extension) | PK(A) — requires A → B | -| `A * B` (join) | Depends on functional dependencies (see below) | - -### Join Primary Key Rule - -The join operator requires special handling because it combines two entity sets. The primary key of `A * B` depends on the **functional dependency relationship** between the operands. - -#### Definitions - -**A determines B** (written `A → B`): Every attribute in PK(B) is in A. - -``` -A → B iff ∀b ∈ PK(B): b ∈ A -``` - -Since `PK(A) ∪ secondary(A) = all attributes in A`, this is equivalent to saying every attribute in B's primary key exists somewhere in A (as either a primary key or secondary attribute). - -Intuitively, `A → B` means that knowing A's primary key is sufficient to determine B's primary key through the functional dependencies implied by A's structure. - -**B determines A** (written `B → A`): Every attribute in PK(A) is in B. - -``` -B → A iff ∀a ∈ PK(A): a ∈ B -``` - -#### Join Primary Key Algorithm - -For `A * B`: - -| Condition | PK(A * B) | Attribute Order | -|-----------|-----------|-----------------| -| A → B | PK(A) | A's attributes first | -| B → A (and not A → B) | PK(B) | B's attributes first | -| Neither | PK(A) ∪ PK(B) | PK(A) first, then PK(B) − PK(A) | - -When both `A → B` and `B → A` hold, the left operand takes precedence (use PK(A)). - -#### Examples - -**Example 1: B → A** -``` -A: x*, y* -B: x*, z*, y (y is secondary in B, so z → y) -``` -- A → B? PK(B) = {x, z}. Is z in PK(A) or secondary in A? No (z not in A). **No.** -- B → A? PK(A) = {x, y}. Is y in PK(B) or secondary in B? Yes (secondary). **Yes.** -- Result: **PK(A * B) = {x, z}** with B's attributes first. - -**Example 2: Both directions (bijection-like)** -``` -A: x*, y*, z (z is secondary in A) -B: y*, z*, x (x is secondary in B) -``` -- A → B? PK(B) = {y, z}. Is z in PK(A) or secondary in A? Yes (secondary). **Yes.** -- B → A? PK(A) = {x, y}. Is x in PK(B) or secondary in B? Yes (secondary). **Yes.** -- Both hold, prefer left operand: **PK(A * B) = {x, y}** with A's attributes first. - -**Example 3: Neither direction** -``` -A: x*, y* -B: z*, x (x is secondary in B) -``` -- A → B? PK(B) = {z}. Is z in PK(A) or secondary in A? No. **No.** -- B → A? PK(A) = {x, y}. Is y in PK(B) or secondary in B? No (y not in B). **No.** -- Result: **PK(A * B) = {x, y, z}** (union) with A's attributes first. - -**Example 4: A → B (subordinate relationship)** -``` -Session: session_id* -Trial: session_id*, trial_num* (references Session) -``` -- A → B? PK(Trial) = {session_id, trial_num}. Is trial_num in PK(Session) or secondary? No. **No.** -- B → A? PK(Session) = {session_id}. Is session_id in PK(Trial)? Yes. **Yes.** -- Result: **PK(Session * Trial) = {session_id, trial_num}** with Trial's attributes first. - -**Join primary key determination**: - - `A * B` where `A → B`: result has PK(A) - - `A * B` where `B → A` (not `A → B`): result has PK(B), B's attributes first - - `A * B` where both `A → B` and `B → A`: result has PK(A) (left preference) - - `A * B` where neither direction: result has PK(A) ∪ PK(B) - - Verify attribute ordering matches primary key source - - Verify non-commutativity: `A * B` vs `B * A` may differ in PK and order - -### Design Tradeoff: Predictability vs. Minimality - -The join primary key rule prioritizes **predictability** over **minimality**. In some cases, the resulting primary key may not be minimal (i.e., it may contain functionally redundant attributes). - -**Example of non-minimal result:** -``` -A: x*, y* -B: z*, x (x is secondary in B, so z → x) -``` - -The mathematically minimal primary key for `A * B` would be `{y, z}` because: -- `z → x` (from B's structure) -- `{y, z} → {x, y, z}` (z gives us x, and we have y) - -However, `{y, z}` is problematic: -- It is **not the primary key of either operand** (A has `{x, y}`, B has `{z}`) -- It is **not the union** of the primary keys -- It represents a **novel entity type** that doesn't correspond to A, B, or their natural pairing - -This creates confusion: what kind of entity does `{y, z}` identify? - -**The simplified rule produces `{x, y, z}`** (the union), which: -- Is immediately recognizable as "one A entity paired with one B entity" -- Contains A's full primary key and B's full primary key -- May have redundancy (`x` is determined by `z`) but is semantically clear - -**Rationale:** Users can always project away redundant attributes if they need the minimal key. But starting with a predictable, interpretable primary key reduces confusion and errors. - -### Attribute Ordering - -The primary key attributes always appear **first** in the result's attribute list, followed by secondary attributes. When `B → A` (and not `A → B`), the join is conceptually reordered as `B * A` to maintain this invariant: - -- If PK = PK(A): A's attributes appear first -- If PK = PK(B): B's attributes appear first -- If PK = PK(A) ∪ PK(B): PK(A) attributes first, then PK(B) − PK(A), then secondaries - -### Non-Commutativity - -With these rules, join is **not commutative** in terms of: -1. **Primary key selection**: `A * B` may have a different PK than `B * A` when one direction determines but not the other -2. **Attribute ordering**: The left operand's attributes appear first (unless B → A) - -The **result set** (the actual rows returned) remains the same regardless of order, but the **schema** (primary key and attribute order) may differ. - -### Left Join Constraint - -For left joins (`A.join(B, left=True)`), the functional dependency **A → B is required**. - -**Why this constraint exists:** - -In a left join, all rows from A are retained even if there's no matching row in B. For unmatched rows, B's attributes are NULL. This creates a problem for primary key validity: - -| Scenario | PK by inner join rule | Left join problem | -|----------|----------------------|-------------------| -| A → B | PK(A) | ✅ Safe — A's attrs always present | -| B → A | PK(B) | ❌ B's PK attrs could be NULL | -| Neither | PK(A) ∪ PK(B) | ❌ B's PK attrs could be NULL | - -**Example of invalid left join:** -``` -A: x*, y* PK(A) = {x, y} -B: x*, z*, y PK(B) = {x, z}, y is secondary - -Inner join: PK = {x, z} (B → A rule) -Left join attempt: FAILS because z could be NULL for unmatched A rows -``` - -**Valid left join example:** -``` -Session: session_id*, date -Trial: session_id*, trial_num*, stimulus (references Session) - -Session.join(Trial, left=True) # OK: Session → Trial -# PK = {session_id}, all sessions retained even without trials -``` - -**Error message:** -``` -DataJointError: Left join requires the left operand to determine the right operand (A → B). -The following attributes from the right operand's primary key are not determined by -the left operand: ['z']. Use an inner join or restructure the query. -``` - -### Conceptual Note: Left Join as Extension - -When `A → B`, the left join `A.join(B, left=True)` is conceptually distinct from the general join operator `A * B`. It is better understood as an **extension** operation rather than a join: - -| Aspect | General Join (A * B) | Left Join when A → B | -|--------|---------------------|----------------------| -| Conceptual model | Cartesian product restricted to matching rows | Extend A with attributes from B | -| Row count | May increase, decrease, or stay same | Always equals len(A) | -| Primary key | Depends on functional dependencies | Always PK(A) | -| Relation to projection | Different operation | Variation of projection | - -**The extension perspective:** - -The operation `A.join(B, left=True)` when `A → B` is closer to **projection** than to **join**: -- It adds new attributes to A (like `A.proj(..., new_attr=...)`) -- It preserves all rows of A -- It preserves A's primary key -- It lacks the Cartesian product aspect that defines joins - -DataJoint provides an explicit `extend()` method for this pattern: - -```python -# These are equivalent when A → B: -A.join(B, left=True) -A.extend(B) # clearer intent: extend A with B's attributes -``` - -The `extend()` method: -- Requires `A → B` (raises `DataJointError` otherwise) -- Does not expose `allow_nullable_pk` (that's an internal mechanism) -- Expresses the semantic intent: "add B's attributes to A's entities" - -**Relationship to aggregation:** - -A similar argument applies to `A.aggr(B, ...)`: -- It preserves A's primary key -- It adds computed attributes derived from B -- It's conceptually a variation of projection with grouping - -Both `A.join(B, left=True)` (when A → B) and `A.aggr(B, ...)` can be viewed as **projection-like operations** that extend A's attributes while preserving its entity identity. - -### Bypassing the Left Join Constraint - -For special cases where the user takes responsibility for handling the potentially nullable primary key, the constraint can be bypassed using `allow_nullable_pk=True`: - -```python -# Normally blocked - A does not determine B -A.join(B, left=True) # Error: A → B not satisfied - -# Bypass the constraint - user takes responsibility -A.join(B, left=True, allow_nullable_pk=True) # Allowed, PK = PK(A) ∪ PK(B) -``` - -When bypassed, the resulting primary key is the union of both operands' primary keys (PK(A) ∪ PK(B)). The user must ensure that subsequent operations (such as `GROUP BY` or projection) establish a valid primary key. The parameter name `allow_nullable_pk` reflects the specific issue: primary key attributes from the right operand could be NULL for unmatched rows. - -This mechanism is used internally by aggregation (`aggr`) with `keep_all_rows=True`, which resets the primary key via the `GROUP BY` clause. - -### Aggregation Exception - -`A.aggr(B, keep_all_rows=True)` uses a left join internally but has the **opposite requirement**: **B → A** (the group expression B must have all of A's primary key attributes). - -This apparent contradiction is resolved by the `GROUP BY` clause: - -1. Aggregation requires B → A so that B can be grouped by A's primary key -2. The intermediate left join `A LEFT JOIN B` would have an invalid PK under the normal left join rules -3. Aggregation internally allows the invalid PK, producing PK(A) ∪ PK(B) -4. The `GROUP BY PK(A)` clause then **resets** the primary key to PK(A) -5. The final result has PK(A), which consists entirely of non-NULL values from A - -Note: The semantic check (homologous namesake validation) is still performed for aggregation's internal join. Only the primary key validity constraint is bypassed. - -**Example:** -``` -Session: session_id*, date -Trial: session_id*, trial_num*, response_time (references Session) - -# Aggregation with keep_all_rows=True -Session.aggr(Trial, keep_all_rows=True, avg_rt='avg(response_time)') - -# Internally: Session LEFT JOIN Trial (with invalid PK allowed) -# Intermediate PK would be {session_id} ∪ {session_id, trial_num} = {session_id, trial_num} -# But GROUP BY session_id resets PK to {session_id} -# Result: All sessions, with avg_rt=NULL for sessions without trials -``` - -## Universal Set `dj.U` - -`dj.U()` or `dj.U('attr1', 'attr2', ...)` represents the universal set of all possible values and lineages. - -### Homology with `dj.U` -Since `dj.U` conceptually contains all possible lineages, its attributes are **homologous to any namesake attribute** in other expressions. - -### Valid Operations - -```python -# Restriction: promotes a, b to PK; lineage transferred from A -dj.U('a', 'b') & A - -# Aggregation: groups by a, b -dj.U('a', 'b').aggr(A, count='count(*)') -``` - -### Invalid Operations - -```python -# Anti-restriction: produces infinite set -dj.U('a', 'b') - A # DataJointError - -# Join: deprecated, use & instead -dj.U('a', 'b') * A # DataJointError with migration guidance -``` - diff --git a/docs/src/archive/design/recall.md b/docs/src/archive/design/recall.md deleted file mode 100644 index 56226cabd..000000000 --- a/docs/src/archive/design/recall.md +++ /dev/null @@ -1,207 +0,0 @@ -# Work with Existing Pipelines - -## Loading Classes - -This section describes how to work with database schemas without access to the -original code that generated the schema. These situations often arise when the -database is created by another user who has not shared the generating code yet -or when the database schema is created from a programming language other than -Python. - -```python -import datajoint as dj -``` - -### Working with schemas and their modules - -Typically a DataJoint schema is created as a dedicated Python module. This -module defines a schema object that is used to link classes declared in the -module to tables in the database schema. As an example, examine the university -module: [university.py](https://github.com/datajoint-company/db-programming-with-datajoint/blob/master/notebooks/university.py). - -You may then import the module to interact with its tables: - -```python -import university as uni -dj.Diagram(uni) -``` - -![query object preview](../images/virtual-module-ERD.svg){: style="align:center"} - -Note that dj.Diagram can extract the diagram from a schema object or from a -Python module containing its schema object, lending further support to the -convention of one-to-one correspondence between database schemas and Python -modules in a DataJoint project: - -`dj.Diagram(uni)` - -is equivalent to - -`dj.Diagram(uni.schema)` - -```python -# students without majors -uni.Student - uni.StudentMajor -``` - -![query object preview](../images/StudentTable.png){: style="align:center"} - -### Spawning missing classes - -Now imagine that you do not have access to `university.py` or you do not have -its latest version. You can still connect to the database schema but you will -not have classes declared to interact with it. - -So let's start over in this scenario. - -You may use the `dj.list_schemas` function (new in DataJoint 0.12.0) to -list the names of database schemas available to you. - -```python -import datajoint as dj -dj.list_schemas() -``` - -```text -*['dimitri_alter','dimitri_attach','dimitri_blob','dimitri_blobs', -'dimitri_nphoton','dimitri_schema','dimitri_university','dimitri_uuid', -'university']* -``` - -Just as with a new schema, we start by creating a schema object to connect to -the chosen database schema: - -```python -schema = dj.Schema('dimitri_university') -``` - -If the schema already exists, `dj.Schema` is initialized as usual and you may plot -the schema diagram. But instead of seeing class names, you will see the raw -table names as they appear in the database. - -```python -# let's plot its diagram -dj.Diagram(schema) -``` - -![query object preview](../images/dimitri-ERD.svg){: style="align:center"} - -You may view the diagram but, at this point, there is no way to interact with -these tables. A similar situation arises when another developer has added new -tables to the schema but has not yet shared the updated module code with you. -Then the diagram will show a mixture of class names and database table names. - -Now you may use the `spawn_missing_classes` method to spawn classes into -the local namespace for any tables missing their classes: - -```python -schema.spawn_missing_classes() -dj.Diagram(schema) -``` - -![query object preview](../images/spawned-classes-ERD.svg){: style="align:center"} - -Now you may interact with these tables as if they were declared right here in -this namespace: - -```python -# students without majors -Student - StudentMajor -``` - -![query object preview](../images/StudentTable.png){: style="align:center"} - -### Creating a virtual module - -Virtual modules provide a way to access the classes corresponding to tables in a -DataJoint schema without having to create local files. - -`spawn_missing_classes` creates the new classes in the local namespace. -However, it is often more convenient to import a schema with its Python module, -equivalent to the Python command: - -```python -import university as uni -``` - -We can mimic this import without having access to `university.py` using the -`VirtualModule` class object: - -```python -import datajoint as dj - -uni = dj.VirtualModule(module_name='university.py', schema_name='dimitri_university') -``` - -Now `uni` behaves as an imported module complete with the schema object and all -the table classes. - -```python -dj.Diagram(uni) -``` - -![query object preview](../images/added-example-ERD.svg){: style="align:center"} - -```python -uni.Student - uni.StudentMajor -``` - -![query object preview](../images/StudentTable.png){: style="align:center"} - -`dj.VirtualModule` takes required arguments - -- `module_name`: displayed module name. - -- `schema_name`: name of the database in MySQL. - -And `dj.VirtualModule` takes optional arguments. - -First, `create_schema=False` assures that an error is raised when the schema -does not already exist. Set it to `True` if you want to create an empty schema. - -```python -dj.VirtualModule('what', 'nonexistent') -``` - -Returns - -```python ---------------------------------------------------------------------------- -DataJointError Traceback (most recent call last) -. -. -. -DataJointError: Database named `nonexistent` was not defined. Set argument create_schema=True to create it. -``` - -The other optional argument, `create_tables=False` is passed to the schema -object. It prevents the use of the schema object of the virtual module for -creating new tables in the existing schema. This is a precautionary measure -since virtual modules are often used for completed schemas. You may set this -argument to `True` if you wish to add new tables to the existing schema. A -more common approach in this scenario would be to create a new schema object and -to use the `spawn_missing_classes` function to make the classes available. - -However, you if do decide to create new tables in an existing tables using the -virtual module, you may do so by using the schema object from the module as the -decorator for declaring new tables: - -```python -uni = dj.VirtualModule('university.py', 'dimitri_university', create_tables=True) -``` - -```python -@uni.schema -class Example(dj.Manual): - definition = """ - -> uni.Student - --- - example : varchar(255) - """ -``` - -```python -dj.Diagram(uni) -``` - -![query object preview](../images/added-example-ERD.svg){: style="align:center"} diff --git a/docs/src/archive/design/schema.md b/docs/src/archive/design/schema.md deleted file mode 100644 index 94bf6cdcc..000000000 --- a/docs/src/archive/design/schema.md +++ /dev/null @@ -1,49 +0,0 @@ -# Schema Creation - -## Schemas - -On the database server, related tables are grouped into a named collection called a **schema**. -This grouping organizes the data and allows control of user access. -A database server may contain multiple schemas each containing a subset of the tables. -A single pipeline may comprise multiple schemas. -Tables are defined within a schema, so a schema must be created before the creation of -any tables. - -By convention, the `datajoint` package is imported as `dj`. - The documentation refers to the package as `dj` throughout. - -Create a new schema using the `dj.Schema` class object: - -```python -import datajoint as dj -schema = dj.Schema('alice_experiment') -``` - -This statement creates the database schema `alice_experiment` on the server. - -The returned object `schema` will then serve as a decorator for DataJoint classes, as -described in [table declaration syntax](./tables/declare.md). - -It is a common practice to have a separate Python module for each schema. -Therefore, each such module has only one `dj.Schema` object defined and is usually -named `schema`. - -The `dj.Schema` constructor can take a number of optional parameters after the schema -name. - -- `context` - Dictionary for looking up foreign key references. - Defaults to `None` to use local context. -- `connection` - Specifies the DataJoint connection object. - Defaults to `dj.conn()`. -- `create_schema` - When `False`, the schema object will not create a schema on the -database and will raise an error if one does not already exist. - Defaults to `True`. -- `create_tables` - When `False`, the schema object will not create tables on the -database and will raise errors when accessing missing tables. - Defaults to `True`. - -## Working with existing data - -See the chapter [recall](recall.md) for how to work with data in -existing pipelines, including accessing a pipeline from one language when the pipeline -was developed using another. diff --git a/docs/src/archive/design/semantic-matching-spec.md b/docs/src/archive/design/semantic-matching-spec.md deleted file mode 100644 index b3333a873..000000000 --- a/docs/src/archive/design/semantic-matching-spec.md +++ /dev/null @@ -1,540 +0,0 @@ -# Semantic Matching for Joins - Specification - -## Overview - -This document specifies **semantic matching** for joins in DataJoint 2.0, replacing the current name-based matching rules. Semantic matching ensures that attributes are only matched when they share both the same name and the same **lineage** (origin), preventing accidental joins on unrelated attributes that happen to share names. - -### Goals - -1. **Prevent incorrect joins** on attributes that share names but represent different entities -2. **Enable valid joins** that are currently blocked due to overly restrictive rules -3. **Maintain backward compatibility** for well-designed schemas -4. **Provide clear error messages** when semantic conflicts are detected - ---- - -## User Guide - -### Quick Start - -Semantic matching is enabled by default in DataJoint 2.0. For most well-designed schemas, no changes are required. - -#### When You Might See Errors - -```python -# Two tables with generic 'id' attribute -class Student(dj.Manual): - definition = """ - id : uint32 - --- - name : varchar(100) - """ - -class Course(dj.Manual): - definition = """ - id : uint32 - --- - title : varchar(100) - """ - -# This will raise an error because 'id' has different lineages -Student() * Course() # DataJointError! -``` - -#### How to Resolve - -**Option 1: Rename attributes using projection** -```python -Student() * Course().proj(course_id='id') # OK -``` - -**Option 2: Bypass semantic check (use with caution)** -```python -Student().join(Course(), semantic_check=False) # OK, but be careful! -``` - -**Option 3: Use descriptive names (best practice)** -```python -class Student(dj.Manual): - definition = """ - student_id : uint32 - --- - name : varchar(100) - """ -``` - -### Migrating from DataJoint 1.x - -#### Removed Operators - -| Old Syntax | New Syntax | -|------------|------------| -| `A @ B` | `A.join(B, semantic_check=False)` | -| `A ^ B` | `A.restrict(B, semantic_check=False)` | -| `dj.U('a') * B` | `dj.U('a') & B` | - -#### Rebuilding Lineage for Existing Schemas - -If you have existing schemas created before DataJoint 2.0, rebuild their lineage tables: - -```python -import datajoint as dj - -# Connect and get your schema -schema = dj.Schema('my_database') - -# Rebuild lineage (do this once per schema) -schema.rebuild_lineage() - -# Restart Python kernel to pick up changes -``` - -**Important**: If your schema references tables in other schemas, rebuild those upstream schemas first. - ---- - -## API Reference - -### Schema Methods - -#### `schema.rebuild_lineage()` - -Rebuild the `~lineage` table for all tables in this schema. - -```python -schema.rebuild_lineage() -``` - -**Description**: Recomputes lineage for all attributes by querying FK relationships from the database's `information_schema`. Use this to restore lineage for schemas that predate the lineage system or after corruption. - -**Requirements**: -- Schema must exist -- Upstream schemas (referenced via cross-schema FKs) must have their lineage rebuilt first - -**Side Effects**: -- Creates `~lineage` table if it doesn't exist -- Deletes and repopulates all lineage entries for tables in the schema - -**Post-Action**: Restart Python kernel and reimport to pick up new lineage information. - -#### `schema.lineage_table_exists` - -Property indicating whether the `~lineage` table exists in this schema. - -```python -if schema.lineage_table_exists: - print("Lineage tracking is enabled") -``` - -**Returns**: `bool` - `True` if `~lineage` table exists, `False` otherwise. - -#### `schema.lineage` - -Property returning all lineage entries for the schema. - -```python -schema.lineage -# {'myschema.session.session_id': 'myschema.session.session_id', -# 'myschema.trial.session_id': 'myschema.session.session_id', -# 'myschema.trial.trial_num': 'myschema.trial.trial_num'} -``` - -**Returns**: `dict` - Maps `'schema.table.attribute'` to its lineage origin - -### Join Methods - -#### `expr.join(other, semantic_check=True)` - -Join two expressions with optional semantic checking. - -```python -result = A.join(B) # semantic_check=True (default) -result = A.join(B, semantic_check=False) # bypass semantic check -``` - -**Parameters**: -- `other`: Another query expression to join with -- `semantic_check` (bool): If `True` (default), raise error on non-homologous namesakes. If `False`, perform natural join without lineage checking. - -**Raises**: `DataJointError` if `semantic_check=True` and namesake attributes have different lineages. - -#### `expr.restrict(other, semantic_check=True)` - -Restrict expression with optional semantic checking. - -```python -result = A.restrict(B) # semantic_check=True (default) -result = A.restrict(B, semantic_check=False) # bypass semantic check -``` - -**Parameters**: -- `other`: Restriction condition (expression, dict, string, etc.) -- `semantic_check` (bool): If `True` (default), raise error on non-homologous namesakes when restricting by another expression. If `False`, no lineage checking. - -**Raises**: `DataJointError` if `semantic_check=True` and namesake attributes have different lineages. - -### Operators - -#### `A * B` (Join) - -Equivalent to `A.join(B, semantic_check=True)`. - -#### `A & B` (Restriction) - -Equivalent to `A.restrict(B, semantic_check=True)`. - -#### `A - B` (Anti-restriction) - -Restriction with negation. Semantic checking applies. - -To bypass semantic checking: `A.restrict(dj.Not(B), semantic_check=False)` - -#### `A + B` (Union) - -Union of expressions. Requires all namesake attributes to have matching lineage. - -### Removed Operators - -#### `A @ B` (Removed) - -Raises `DataJointError` with migration guidance to use `.join(semantic_check=False)`. - -#### `A ^ B` (Removed) - -Raises `DataJointError` with migration guidance to use `.restrict(semantic_check=False)`. - -#### `dj.U(...) * A` (Removed) - -Raises `DataJointError` with migration guidance to use `dj.U(...) & A`. - -### Universal Set (`dj.U`) - -#### Valid Operations - -```python -dj.U('a', 'b') & A # Restriction: promotes a, b to PK -dj.U('a', 'b').aggr(A, ...) # Aggregation: groups by a, b -dj.U() & A # Distinct primary keys of A -``` - -#### Invalid Operations - -```python -dj.U('a', 'b') - A # DataJointError: produces infinite set -dj.U('a', 'b') * A # DataJointError: use & instead -``` - ---- - -## Concepts - -### Attribute Lineage - -Lineage identifies the **origin** of an attribute - where it was first defined. It is represented as a string: - -``` -schema_name.table_name.attribute_name -``` - -#### Lineage Assignment Rules - -| Attribute Type | Lineage Value | -|----------------|---------------| -| Native primary key | `this_schema.this_table.attr_name` | -| FK-inherited (primary or secondary) | Traced to original definition | -| Native secondary | `None` | -| Computed (in projection) | `None` | - -#### Example - -```python -class Session(dj.Manual): # table: session - definition = """ - session_id : uint32 - --- - session_date : date - """ - -class Trial(dj.Manual): # table: trial - definition = """ - -> Session - trial_num : uint16 - --- - stimulus : varchar(100) - """ -``` - -Lineages: -- `Session.session_id` → `myschema.session.session_id` (native PK) -- `Session.session_date` → `None` (native secondary) -- `Trial.session_id` → `myschema.session.session_id` (inherited via FK) -- `Trial.trial_num` → `myschema.trial.trial_num` (native PK) -- `Trial.stimulus` → `None` (native secondary) - -### Terminology - -| Term | Definition | -|------|------------| -| **Lineage** | The origin of an attribute: `schema.table.attribute` | -| **Homologous attributes** | Attributes with the same lineage | -| **Namesake attributes** | Attributes with the same name | -| **Homologous namesakes** | Same name AND same lineage — used for join matching | -| **Non-homologous namesakes** | Same name BUT different lineage — cause join errors | - -### Semantic Matching Rules - -| Scenario | Action | -|----------|--------| -| Same name, same lineage (both non-null) | **Match** | -| Same name, different lineage | **Error** | -| Same name, either lineage is null | **Error** | -| Different names | **No match** | - ---- - -## Implementation Details - -### `~lineage` Table - -Each schema has a hidden `~lineage` table storing lineage information: - -```sql -CREATE TABLE `schema_name`.`~lineage` ( - table_name VARCHAR(64) NOT NULL, - attribute_name VARCHAR(64) NOT NULL, - lineage VARCHAR(255) NOT NULL, - PRIMARY KEY (table_name, attribute_name) -) -``` - -### Lineage Population - -**At table declaration**: -1. Delete any existing lineage entries for the table -2. For FK attributes: copy lineage from parent (with warning if parent lineage missing) -3. For native PK attributes: set lineage to `schema.table.attribute` -4. Native secondary attributes: no entry (lineage = None) - -**At table drop**: -- Delete all lineage entries for the table - -### Missing Lineage Handling - -**If `~lineage` table doesn't exist**: -- Warning issued during semantic check -- Semantic checking disabled (join proceeds as natural join) - -**If parent lineage missing during declaration**: -- Warning issued -- Parent attribute used as origin -- Recommend rebuilding lineage after parent schema is fixed - -### Heading's `lineage_available` Property - -The `Heading` class tracks whether lineage information is available: - -```python -heading.lineage_available # True if ~lineage table exists for this schema -``` - -This property is: -- Set when heading is loaded from database -- Propagated through projections, joins, and other operations -- Used by `assert_join_compatibility` to decide whether to perform semantic checking - ---- - -## Error Messages - -### Non-Homologous Namesakes - -``` -DataJointError: Cannot join on attribute `id`: different lineages -(university.student.id vs university.course.id). -Use .proj() to rename one of the attributes. -``` - -### Removed `@` Operator - -``` -DataJointError: The @ operator has been removed in DataJoint 2.0. -Use .join(other, semantic_check=False) for permissive joins. -``` - -### Removed `^` Operator - -``` -DataJointError: The ^ operator has been removed in DataJoint 2.0. -Use .restrict(other, semantic_check=False) for permissive restrictions. -``` - -### Removed `dj.U * table` - -``` -DataJointError: dj.U(...) * table is no longer supported in DataJoint 2.0. -Use dj.U(...) & table instead. -``` - -### Missing Lineage Warning - -``` -WARNING: Semantic check disabled: ~lineage table not found. -To enable semantic matching, rebuild lineage with: schema.rebuild_lineage() -``` - -### Parent Lineage Missing Warning - -``` -WARNING: Lineage for `parent_db`.`parent_table`.`attr` not found -(parent schema's ~lineage table may be missing or incomplete). -Using it as origin. Once the parent schema's lineage is rebuilt, -run schema.rebuild_lineage() on this schema to correct the lineage. -``` - ---- - -## Examples - -### Example 1: Valid Join (Shared Lineage) - -```python -class Student(dj.Manual): - definition = """ - student_id : uint32 - --- - name : varchar(100) - """ - -class Enrollment(dj.Manual): - definition = """ - -> Student - -> Course - --- - grade : varchar(2) - """ - -# Works: student_id has same lineage in both -Student() * Enrollment() -``` - -### Example 2: Invalid Join (Different Lineage) - -```python -class TableA(dj.Manual): - definition = """ - id : uint32 - --- - value_a : int32 - """ - -class TableB(dj.Manual): - definition = """ - id : uint32 - --- - value_b : int32 - """ - -# Error: 'id' has different lineages -TableA() * TableB() - -# Solution 1: Rename -TableA() * TableB().proj(b_id='id') - -# Solution 2: Bypass (use with caution) -TableA().join(TableB(), semantic_check=False) -``` - -### Example 3: Multi-hop FK Inheritance - -```python -class Session(dj.Manual): - definition = """ - session_id : uint32 - --- - session_date : date - """ - -class Trial(dj.Manual): - definition = """ - -> Session - trial_num : uint16 - """ - -class Response(dj.Computed): - definition = """ - -> Trial - --- - response_time : float64 - """ - -# All work: session_id traces back to Session in all tables -Session() * Trial() -Session() * Response() -Trial() * Response() -``` - -### Example 4: Secondary FK Attribute - -```python -class Course(dj.Manual): - definition = """ - course_id : int unsigned - --- - title : varchar(100) - """ - -class FavoriteCourse(dj.Manual): - definition = """ - student_id : int unsigned - --- - -> Course - """ - -class RequiredCourse(dj.Manual): - definition = """ - major_id : int unsigned - --- - -> Course - """ - -# Works: course_id is secondary in both, but has same lineage -FavoriteCourse() * RequiredCourse() -``` - -### Example 5: Aliased Foreign Key - -```python -class Person(dj.Manual): - definition = """ - person_id : int unsigned - --- - full_name : varchar(100) - """ - -class Marriage(dj.Manual): - definition = """ - -> Person.proj(husband='person_id') - -> Person.proj(wife='person_id') - --- - marriage_date : date - """ - -# husband and wife both have lineage: schema.person.person_id -# They are homologous (same lineage) but have different names -``` - ---- - -## Best Practices - -1. **Use descriptive attribute names**: Prefer `student_id` over generic `id` - -2. **Leverage foreign keys**: Inherited attributes maintain lineage automatically - -3. **Rebuild lineage for legacy schemas**: Run `schema.rebuild_lineage()` once - -4. **Rebuild upstream schemas first**: For cross-schema FKs, rebuild parent schemas before child schemas - -5. **Restart after rebuilding**: Restart Python kernel to pick up new lineage information - -6. **Use `semantic_check=False` sparingly**: Only when you're certain the natural join is correct diff --git a/docs/src/archive/design/tables/attach.md b/docs/src/archive/design/tables/attach.md deleted file mode 100644 index c4950ffdf..000000000 --- a/docs/src/archive/design/tables/attach.md +++ /dev/null @@ -1,67 +0,0 @@ -# External Data - -## File Attachment Datatype - -### Configuration & Usage - -Corresponding to issue -[#480](https://github.com/datajoint/datajoint-python/issues/480), -the `attach` attribute type allows users to `attach` files into DataJoint -schemas as DataJoint-managed files. This is in contrast to traditional `blobs` -which are encodings of programming language data structures such as arrays. - -The functionality is modeled after email attachments, where users `attach` -a file along with a message and message recipients have access to a -copy of that file upon retrieval of the message. - -For DataJoint `attach` attributes, DataJoint will copy the input -file into a DataJoint store, hash the file contents, and track -the input file name. Subsequent `fetch` operations will transfer a -copy of the file to the local directory of the Python process and -return a pointer to it's location for subsequent client usage. This -allows arbitrary files to be `uploaded` or `attached` to a DataJoint -schema for later use in processing. File integrity is preserved by -checksum comparison against the attachment data and verifying the contents -during retrieval. - -For example, given a `localattach` store: - -```python -dj.config['stores'] = { - 'localattach': { - 'protocol': 'file', - 'location': '/data/attach' - } -} -``` - -A `ScanAttachment` table can be created: - -```python -@schema -class ScanAttachment(dj.Manual): - definition = """ - -> Session - --- - scan_image: attach@localattach # attached image scans - """ -``` - -Files can be added using an insert pointing to the source file: - -```python ->>> ScanAttachment.insert1((0, '/input/image0.tif')) -``` - -And then retrieved to the current directory using `fetch`: - -```python ->>> s0 = (ScanAttachment & {'session_id': 0}).fetch1() ->>> s0 -{'session_id': 0, 'scan_image': './image0.tif'} ->>> fh = open(s0['scan_image'], 'rb') ->>> fh -<_io.BufferedReader name='./image0.tif') -``` - - diff --git a/docs/src/archive/design/tables/attributes.md b/docs/src/archive/design/tables/attributes.md deleted file mode 100644 index 3753621d5..000000000 --- a/docs/src/archive/design/tables/attributes.md +++ /dev/null @@ -1,181 +0,0 @@ -# Datatypes - -DataJoint supports the following datatypes. -To conserve database resources, use the smallest and most restrictive datatype -sufficient for your data. -This also ensures that only valid data are entered into the pipeline. - -## Core datatypes (recommended) - -Use these portable, scientist-friendly types for cross-database compatibility. - -### Integers - -- `int8`: 8-bit signed integer (-128 to 127) -- `uint8`: 8-bit unsigned integer (0 to 255) -- `int16`: 16-bit signed integer (-32,768 to 32,767) -- `uint16`: 16-bit unsigned integer (0 to 65,535) -- `int32`: 32-bit signed integer -- `uint32`: 32-bit unsigned integer -- `int64`: 64-bit signed integer -- `uint64`: 64-bit unsigned integer -- `bool`: boolean value (True/False, stored as 0/1) - -### Floating-point - -- `float32`: 32-bit single-precision floating-point. Sufficient for many measurements. -- `float64`: 64-bit double-precision floating-point. - Avoid using floating-point types in primary keys due to equality comparison issues. -- `decimal(n,f)`: fixed-point number with *n* total digits and *f* fractional digits. - Use for exact decimal representation (e.g., currency, coordinates). - Safe for primary keys due to well-defined precision. - -### Strings - -- `char(n)`: fixed-length string of exactly *n* characters. -- `varchar(n)`: variable-length string up to *n* characters. -- `enum(...)`: one of several enumerated values, e.g., `enum("low", "medium", "high")`. - Do not use enums in primary keys due to difficulty changing definitions. - -> **Note:** For unlimited text, use `varchar` with a generous limit, `json` for structured content, -> or `` for large text files. Native SQL `text` types are supported but not portable. - -**Encoding policy:** All strings use UTF-8 encoding (`utf8mb4` in MySQL, `UTF8` in PostgreSQL). -Character encoding and collation are database-level configuration, not part of type definitions. -Comparisons are case-sensitive by default. - -### Date/Time - -- `date`: date as `'YYYY-MM-DD'`. -- `datetime`: date and time as `'YYYY-MM-DD HH:MM:SS'`. - Use `CURRENT_TIMESTAMP` as default for auto-populated timestamps. - -**Timezone policy:** All `datetime` values should be stored as **UTC**. Timezone -conversion is a presentation concern handled by the application layer. This ensures -reproducible computations regardless of server location or timezone settings. - -### Binary - -- `bytes`: raw binary data (up to 4 GiB). Stores and returns raw bytes without - serialization. For serialized Python objects (arrays, dicts, etc.), use ``. - -### Other - -- `uuid`: 128-bit universally unique identifier. -- `json`: JSON document for structured data. - -## Native datatypes (advanced) - -Native database types are available for advanced use cases but are **not recommended** -for portable pipelines. Using native types will generate a warning. - -- `tinyint`, `smallint`, `int`, `bigint` (with optional `unsigned`) -- `float`, `double`, `real` -- `tinyblob`, `blob`, `mediumblob`, `longblob` -- `tinytext`, `mediumtext`, `longtext` (size variants) -- `time`, `timestamp`, `year` -- `mediumint`, `serial`, `int auto_increment` - -See the [storage types spec](storage-types-spec.md) for complete mappings. - -## Codec types (special datatypes) - -Codecs provide `encode()`/`decode()` semantics for complex data that doesn't -fit native database types. They are denoted with angle brackets: ``. - -### Storage mode: `@` convention - -The `@` character indicates **external storage** (object store vs database): - -- **No `@`**: Internal storage (database) - e.g., ``, `` -- **`@` present**: External storage (object store) - e.g., ``, `` -- **`@` alone**: Use default store - e.g., `` -- **`@name`**: Use named store - e.g., `` - -### Built-in codecs - -**Serialization types** - for Python objects: - -- ``: DataJoint's native serialization format for Python objects. Supports - NumPy arrays, dicts, lists, datetime objects, and nested structures. Stores in - database. Compatible with MATLAB. See [custom codecs](codecs.md) for details. - -- `` / ``: Like `` but stores externally with hash- - addressed deduplication. Use for large arrays that may be duplicated across rows. - -**File storage types** - for managed files: - -- `` / ``: Managed file and folder storage with path derived - from primary key. Supports Zarr, HDF5, and direct writes via fsspec. Returns - `ObjectRef` for lazy access. External only. See [object storage](object.md). - -- `` / ``: Hash-addressed storage for raw bytes with - MD5 deduplication. External only. Use via `` or `` rather than directly. - -**File attachment types** - for file transfer: - -- ``: File attachment stored in database with filename preserved. Similar - to email attachments. Good for small files (<16MB). See [attachments](attach.md). - -- `` / ``: Like `` but stores externally with - deduplication. Use for large files. - -**File reference types** - for external files: - -- ``: Reference to existing file in a configured store. No file - copying occurs. Returns `ObjectRef` for lazy access. External only. See [filepath](filepath.md). - -### User-defined codecs - -- ``: Define your own [custom codec](codecs.md) with - bidirectional conversion between Python objects and database storage. Use for - graphs, domain-specific objects, or custom data structures. - -## Core type aliases - -DataJoint provides convenient type aliases that map to standard database types. -These aliases use familiar naming conventions from NumPy and other numerical computing -libraries, making table definitions more readable and portable across database backends. - -| Alias | MySQL | PostgreSQL | Description | -|-------|-------|------------|-------------| -| `bool` | `TINYINT` | `BOOLEAN` | Boolean value (0 or 1) | -| `int8` | `TINYINT` | `SMALLINT` | 8-bit signed integer (-128 to 127) | -| `uint8` | `TINYINT UNSIGNED` | `SMALLINT` | 8-bit unsigned integer (0 to 255) | -| `int16` | `SMALLINT` | `SMALLINT` | 16-bit signed integer | -| `uint16` | `SMALLINT UNSIGNED` | `INTEGER` | 16-bit unsigned integer | -| `int32` | `INT` | `INTEGER` | 32-bit signed integer | -| `uint32` | `INT UNSIGNED` | `BIGINT` | 32-bit unsigned integer | -| `int64` | `BIGINT` | `BIGINT` | 64-bit signed integer | -| `uint64` | `BIGINT UNSIGNED` | `NUMERIC(20)` | 64-bit unsigned integer | -| `float32` | `FLOAT` | `REAL` | 32-bit single-precision float | -| `float64` | `DOUBLE` | `DOUBLE PRECISION` | 64-bit double-precision float | -| `bytes` | `LONGBLOB` | `BYTEA` | Raw binary data | - -Example usage: - -```python -@schema -class Measurement(dj.Manual): - definition = """ - measurement_id : int32 - --- - temperature : float32 # single-precision temperature reading - precise_value : float64 # double-precision measurement - sample_count : uint32 # unsigned 32-bit counter - sensor_flags : uint8 # 8-bit status flags - is_valid : bool # boolean flag - raw_data : bytes # raw binary data - processed : # serialized Python object - large_array : # external storage with deduplication - """ -``` - -## Datatypes not (yet) supported - -- `binary(n)` / `varbinary(n)` - use `bytes` instead -- `bit(n)` - use `int` types with bitwise operations -- `set(...)` - use `json` for multiple selections - -For additional information about these datatypes, see -http://dev.mysql.com/doc/refman/5.6/en/data-types.html diff --git a/docs/src/archive/design/tables/blobs.md b/docs/src/archive/design/tables/blobs.md deleted file mode 100644 index 9f73d54d4..000000000 --- a/docs/src/archive/design/tables/blobs.md +++ /dev/null @@ -1,26 +0,0 @@ -# Blobs - -DataJoint provides functionality for serializing and deserializing complex data types -into binary blobs for efficient storage and compatibility with MATLAB's mYm -serialization. This includes support for: - -+ Basic Python data types (e.g., integers, floats, strings, dictionaries). -+ NumPy arrays and scalars. -+ Specialized data types like UUIDs, decimals, and datetime objects. - -## Serialization and Deserialization Process - -Serialization converts Python objects into a binary representation for efficient storage -within the database. Deserialization converts the binary representation back into the -original Python object. - -Blobs over 1 KiB are compressed using the zlib library to reduce storage requirements. - -## Supported Data Types - -DataJoint supports the following data types for serialization: - -+ Scalars: Integers, floats, booleans, strings. -+ Collections: Lists, tuples, sets, dictionaries. -+ NumPy: Arrays, structured arrays, and scalars. -+ Custom Types: UUIDs, decimals, datetime objects, MATLAB cell and struct arrays. diff --git a/docs/src/archive/design/tables/codec-spec.md b/docs/src/archive/design/tables/codec-spec.md deleted file mode 100644 index a3eefa578..000000000 --- a/docs/src/archive/design/tables/codec-spec.md +++ /dev/null @@ -1,766 +0,0 @@ -# Codec Specification - -This document specifies the DataJoint Codec API for creating custom attribute types -that extend DataJoint's native type system. - -## Overview - -Codecs define bidirectional conversion between Python objects and database storage. -They enable storing complex data types (graphs, models, custom formats) while -maintaining DataJoint's query capabilities. - -``` -┌─────────────────┐ ┌─────────────────┐ -│ Python Object │ ──── encode ────► │ Storage Type │ -│ (e.g. Graph) │ │ (e.g. bytes) │ -│ │ ◄─── decode ──── │ │ -└─────────────────┘ └─────────────────┘ -``` - -## Quick Start - -```python -import datajoint as dj -import networkx as nx - -class GraphCodec(dj.Codec): - """Store NetworkX graphs.""" - - name = "graph" # Use as in definitions - - def get_dtype(self, is_external: bool) -> str: - return "" # Delegate to blob for serialization - - def encode(self, graph, *, key=None, store_name=None): - return { - 'nodes': list(graph.nodes(data=True)), - 'edges': list(graph.edges(data=True)), - } - - def decode(self, stored, *, key=None): - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - G.add_edges_from(stored['edges']) - return G - -# Use in table definition -@schema -class Connectivity(dj.Manual): - definition = ''' - conn_id : int - --- - network : - ''' -``` - -## The Codec Base Class - -All custom codecs inherit from `dj.Codec`: - -```python -class Codec(ABC): - """Base class for codec types.""" - - name: str | None = None # Required: unique identifier - - def get_dtype(self, is_external: bool) -> str: - """Return the storage dtype.""" - raise NotImplementedError - - @abstractmethod - def encode(self, value, *, key=None, store_name=None) -> Any: - """Encode Python value for storage.""" - ... - - @abstractmethod - def decode(self, stored, *, key=None) -> Any: - """Decode stored value back to Python.""" - ... - - def validate(self, value) -> None: - """Optional: validate value before encoding.""" - pass -``` - -## Required Components - -### 1. The `name` Attribute - -The `name` class attribute is a unique identifier used in table definitions with -`` syntax: - -```python -class MyCodec(dj.Codec): - name = "mycodec" # Use as in definitions -``` - -Naming conventions: -- Use lowercase with underscores: `spike_train`, `graph_embedding` -- Avoid generic names that might conflict: prefer `lab_model` over `model` -- Names must be unique across all registered codecs - -### 2. The `get_dtype()` Method - -Returns the underlying storage type. The `is_external` parameter indicates whether -the `@` modifier is present in the table definition: - -```python -def get_dtype(self, is_external: bool) -> str: - """ - Args: - is_external: True if @ modifier present (e.g., ) - - Returns: - - A core type: "bytes", "json", "varchar(N)", "int32", etc. - - Another codec: "", "", etc. - - Raises: - DataJointError: If external storage not supported but @ is present - """ -``` - -Examples: - -```python -# Simple: always store as bytes -def get_dtype(self, is_external: bool) -> str: - return "bytes" - -# Different behavior for internal/external -def get_dtype(self, is_external: bool) -> str: - return "" if is_external else "bytes" - -# External-only codec -def get_dtype(self, is_external: bool) -> str: - if not is_external: - raise DataJointError(" requires @ (external storage only)") - return "json" -``` - -### 3. The `encode()` Method - -Converts Python objects to the format expected by `get_dtype()`: - -```python -def encode(self, value: Any, *, key: dict | None = None, store_name: str | None = None) -> Any: - """ - Args: - value: The Python object to store - key: Primary key values (for context-dependent encoding) - store_name: Target store name (for external storage) - - Returns: - Value in the format expected by get_dtype() - """ -``` - -### 4. The `decode()` Method - -Converts stored values back to Python objects: - -```python -def decode(self, stored: Any, *, key: dict | None = None) -> Any: - """ - Args: - stored: Data retrieved from storage - key: Primary key values (for context-dependent decoding) - - Returns: - The reconstructed Python object - """ -``` - -### 5. The `validate()` Method (Optional) - -Called automatically before `encode()` during INSERT operations: - -```python -def validate(self, value: Any) -> None: - """ - Args: - value: The value to validate - - Raises: - TypeError: If the value has an incompatible type - ValueError: If the value fails domain validation - """ - if not isinstance(value, ExpectedType): - raise TypeError(f"Expected ExpectedType, got {type(value).__name__}") -``` - -## Auto-Registration - -Codecs automatically register when their class is defined. No decorator needed: - -```python -# This codec is registered automatically when the class is defined -class MyCodec(dj.Codec): - name = "mycodec" - # ... -``` - -### Skipping Registration - -For abstract base classes that shouldn't be registered: - -```python -class BaseCodec(dj.Codec, register=False): - """Abstract base - not registered.""" - name = None # Or omit entirely - -class ConcreteCodec(BaseCodec): - name = "concrete" # This one IS registered - # ... -``` - -### Registration Timing - -Codecs are registered at class definition time. Ensure your codec classes are -imported before any table definitions that use them: - -```python -# myproject/codecs.py -class GraphCodec(dj.Codec): - name = "graph" - ... - -# myproject/tables.py -import myproject.codecs # Ensure codecs are registered - -@schema -class Networks(dj.Manual): - definition = ''' - id : int - --- - network : - ''' -``` - -## Codec Composition (Chaining) - -Codecs can delegate to other codecs by returning `` from `get_dtype()`. -This enables layered functionality: - -```python -class CompressedJsonCodec(dj.Codec): - """Compress JSON data with zlib.""" - - name = "zjson" - - def get_dtype(self, is_external: bool) -> str: - return "" # Delegate serialization to blob codec - - def encode(self, value, *, key=None, store_name=None): - import json, zlib - json_bytes = json.dumps(value).encode('utf-8') - return zlib.compress(json_bytes) - - def decode(self, stored, *, key=None): - import json, zlib - json_bytes = zlib.decompress(stored) - return json.loads(json_bytes.decode('utf-8')) -``` - -### How Chaining Works - -When DataJoint encounters ``: - -1. Calls `ZjsonCodec.get_dtype(is_external=False)` → returns `""` -2. Calls `BlobCodec.get_dtype(is_external=False)` → returns `"bytes"` -3. Final storage type is `bytes` (LONGBLOB in MySQL) - -During INSERT: -1. `ZjsonCodec.encode()` converts Python dict → compressed bytes -2. `BlobCodec.encode()` packs bytes → DJ blob format -3. Stored in database - -During FETCH: -1. Read from database -2. `BlobCodec.decode()` unpacks DJ blob → compressed bytes -3. `ZjsonCodec.decode()` decompresses → Python dict - -### Built-in Codec Chains - -DataJoint's built-in codecs form these chains: - -``` - → bytes (internal) - → json (external) - - → bytes (internal) - → json (external) - - → json (external only) - → json (external only) - → json (external only) -``` - -### Store Name Propagation - -When using external storage (`@`), the store name propagates through the chain: - -```python -# Table definition -data : - -# Resolution: -# 1. MyCodec.get_dtype(is_external=True) → "" -# 2. BlobCodec.get_dtype(is_external=True) → "" -# 3. HashCodec.get_dtype(is_external=True) → "json" -# 4. store_name="coldstore" passed to HashCodec.encode() -``` - -## Plugin System (Entry Points) - -Codecs can be distributed as installable packages using Python entry points. - -### Package Structure - -``` -dj-graph-codecs/ -├── pyproject.toml -└── src/ - └── dj_graph_codecs/ - ├── __init__.py - └── codecs.py -``` - -### pyproject.toml - -```toml -[project] -name = "dj-graph-codecs" -version = "1.0.0" -dependencies = ["datajoint>=2.0", "networkx"] - -[project.entry-points."datajoint.codecs"] -graph = "dj_graph_codecs.codecs:GraphCodec" -weighted_graph = "dj_graph_codecs.codecs:WeightedGraphCodec" -``` - -### Codec Implementation - -```python -# src/dj_graph_codecs/codecs.py -import datajoint as dj -import networkx as nx - -class GraphCodec(dj.Codec): - name = "graph" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, graph, *, key=None, store_name=None): - return { - 'nodes': list(graph.nodes(data=True)), - 'edges': list(graph.edges(data=True)), - } - - def decode(self, stored, *, key=None): - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - G.add_edges_from(stored['edges']) - return G - -class WeightedGraphCodec(dj.Codec): - name = "weighted_graph" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, graph, *, key=None, store_name=None): - return { - 'nodes': list(graph.nodes(data=True)), - 'edges': [(u, v, d) for u, v, d in graph.edges(data=True)], - } - - def decode(self, stored, *, key=None): - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - for u, v, d in stored['edges']: - G.add_edge(u, v, **d) - return G -``` - -### Usage After Installation - -```bash -pip install dj-graph-codecs -``` - -```python -# Codecs are automatically discovered and available -@schema -class Networks(dj.Manual): - definition = ''' - network_id : int - --- - topology : - weights : - ''' -``` - -### Entry Point Discovery - -DataJoint loads entry points lazily when a codec is first requested: - -1. Check explicit registry (codecs defined in current process) -2. Load entry points from `datajoint.codecs` group -3. Also checks legacy `datajoint.types` group for compatibility - -## API Reference - -### Module Functions - -```python -import datajoint as dj - -# List all registered codec names -dj.list_codecs() # Returns: ['blob', 'hash', 'object', 'attach', 'filepath', ...] - -# Get a codec instance by name -codec = dj.get_codec("blob") -codec = dj.get_codec("") # Angle brackets are optional -codec = dj.get_codec("") # Store parameter is stripped -``` - -### Internal Functions (for advanced use) - -```python -from datajoint.codecs import ( - is_codec_registered, # Check if codec exists - unregister_codec, # Remove codec (testing only) - resolve_dtype, # Resolve codec chain - parse_type_spec, # Parse "" syntax -) -``` - -## Built-in Codecs - -DataJoint provides these built-in codecs: - -| Codec | Internal | External | Description | -|-------|----------|----------|-------------| -| `` | `bytes` | `` | DataJoint serialization for Python objects | -| `` | N/A | `json` | Content-addressed storage with MD5 deduplication | -| `` | N/A | `json` | Path-addressed storage for files/folders | -| `` | `bytes` | `` | File attachments with filename preserved | -| `` | N/A | `json` | Reference to existing files in store | - -## Complete Examples - -### Example 1: Simple Serialization - -```python -import datajoint as dj -import numpy as np - -class SpikeTrainCodec(dj.Codec): - """Efficient storage for sparse spike timing data.""" - - name = "spike_train" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def validate(self, value): - if not isinstance(value, np.ndarray): - raise TypeError("Expected numpy array of spike times") - if value.ndim != 1: - raise ValueError("Spike train must be 1-dimensional") - if len(value) > 1 and not np.all(np.diff(value) >= 0): - raise ValueError("Spike times must be sorted") - - def encode(self, spike_times, *, key=None, store_name=None): - # Store as differences (smaller values, better compression) - return np.diff(spike_times, prepend=0).astype(np.float32) - - def decode(self, stored, *, key=None): - # Reconstruct original spike times - return np.cumsum(stored).astype(np.float64) -``` - -### Example 2: External Storage - -```python -import datajoint as dj -import pickle - -class ModelCodec(dj.Codec): - """Store ML models with optional external storage.""" - - name = "model" - - def get_dtype(self, is_external: bool) -> str: - # Use hash-addressed storage for large models - return "" if is_external else "" - - def encode(self, model, *, key=None, store_name=None): - return pickle.dumps(model, protocol=pickle.HIGHEST_PROTOCOL) - - def decode(self, stored, *, key=None): - return pickle.loads(stored) - - def validate(self, value): - # Check that model has required interface - if not hasattr(value, 'predict'): - raise TypeError("Model must have a predict() method") -``` - -Usage: -```python -@schema -class Models(dj.Manual): - definition = ''' - model_id : int - --- - small_model : # Internal storage - large_model : # External (default store) - archive_model : # External (specific store) - ''' -``` - -### Example 3: JSON with Schema Validation - -```python -import datajoint as dj -import jsonschema - -class ConfigCodec(dj.Codec): - """Store validated JSON configuration.""" - - name = "config" - - SCHEMA = { - "type": "object", - "properties": { - "version": {"type": "integer", "minimum": 1}, - "settings": {"type": "object"}, - }, - "required": ["version", "settings"], - } - - def get_dtype(self, is_external: bool) -> str: - return "json" - - def validate(self, value): - jsonschema.validate(value, self.SCHEMA) - - def encode(self, config, *, key=None, store_name=None): - return config # JSON type handles serialization - - def decode(self, stored, *, key=None): - return stored -``` - -### Example 4: Context-Dependent Encoding - -```python -import datajoint as dj - -class VersionedDataCodec(dj.Codec): - """Handle different encoding versions based on primary key.""" - - name = "versioned" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, value, *, key=None, store_name=None): - version = key.get("schema_version", 1) if key else 1 - if version >= 2: - return {"v": 2, "data": self._encode_v2(value)} - return {"v": 1, "data": self._encode_v1(value)} - - def decode(self, stored, *, key=None): - version = stored.get("v", 1) - if version >= 2: - return self._decode_v2(stored["data"]) - return self._decode_v1(stored["data"]) - - def _encode_v1(self, value): - return value - - def _decode_v1(self, data): - return data - - def _encode_v2(self, value): - # New encoding format - return {"optimized": True, "payload": value} - - def _decode_v2(self, data): - return data["payload"] -``` - -### Example 5: External-Only Codec - -```python -import datajoint as dj -from pathlib import Path - -class ZarrCodec(dj.Codec): - """Store Zarr arrays in object storage.""" - - name = "zarr" - - def get_dtype(self, is_external: bool) -> str: - if not is_external: - raise dj.DataJointError(" requires @ (external storage only)") - return "" # Delegate to object storage - - def encode(self, value, *, key=None, store_name=None): - import zarr - import tempfile - - # If already a path, pass through - if isinstance(value, (str, Path)): - return str(value) - - # If zarr array, save to temp and return path - if isinstance(value, zarr.Array): - tmpdir = tempfile.mkdtemp() - path = Path(tmpdir) / "data.zarr" - zarr.save(path, value) - return str(path) - - raise TypeError(f"Expected zarr.Array or path, got {type(value)}") - - def decode(self, stored, *, key=None): - # ObjectCodec returns ObjectRef, use its fsmap for zarr - import zarr - return zarr.open(stored.fsmap, mode='r') -``` - -## Best Practices - -### 1. Choose Appropriate Storage Types - -| Data Type | Recommended `get_dtype()` | -|-----------|---------------------------| -| Python objects (dicts, arrays) | `""` | -| Large binary data | `""` (external) | -| Files/folders (Zarr, HDF5) | `""` (external) | -| Simple JSON-serializable | `"json"` | -| Short strings | `"varchar(N)"` | -| Numeric identifiers | `"int32"`, `"int64"` | - -### 2. Handle None Values - -Nullable columns may pass `None` to your codec: - -```python -def encode(self, value, *, key=None, store_name=None): - if value is None: - return None # Pass through for nullable columns - return self._actual_encode(value) - -def decode(self, stored, *, key=None): - if stored is None: - return None - return self._actual_decode(stored) -``` - -### 3. Test Round-Trips - -Always verify that `decode(encode(x)) == x`: - -```python -def test_codec_roundtrip(): - codec = MyCodec() - - test_values = [ - {"key": "value"}, - [1, 2, 3], - np.array([1.0, 2.0]), - ] - - for original in test_values: - encoded = codec.encode(original) - decoded = codec.decode(encoded) - assert decoded == original or np.array_equal(decoded, original) -``` - -### 4. Include Validation - -Catch errors early with `validate()`: - -```python -def validate(self, value): - if not isinstance(value, ExpectedType): - raise TypeError(f"Expected ExpectedType, got {type(value).__name__}") - - if not self._is_valid(value): - raise ValueError("Value fails validation constraints") -``` - -### 5. Document Expected Formats - -Include docstrings explaining input/output formats: - -```python -class MyCodec(dj.Codec): - """ - Store MyType objects. - - Input format (encode): - MyType instance with attributes: x, y, z - - Storage format: - Dict with keys: 'x', 'y', 'z' - - Output format (decode): - MyType instance reconstructed from storage - """ -``` - -### 6. Consider Versioning - -If your encoding format might change: - -```python -def encode(self, value, *, key=None, store_name=None): - return { - "_version": 2, - "_data": self._encode_v2(value), - } - -def decode(self, stored, *, key=None): - version = stored.get("_version", 1) - data = stored.get("_data", stored) - - if version == 1: - return self._decode_v1(data) - return self._decode_v2(data) -``` - -## Error Handling - -### Common Errors - -| Error | Cause | Solution | -|-------|-------|----------| -| `Unknown codec: ` | Codec not registered | Import module defining codec before table definition | -| `Codec already registered` | Duplicate name | Use unique names; check for conflicts | -| ` requires @` | External-only codec used without @ | Add `@` or `@store` to attribute type | -| `Circular codec reference` | Codec chain forms a loop | Check `get_dtype()` return values | - -### Debugging - -```python -# Check what codecs are registered -print(dj.list_codecs()) - -# Inspect a codec -codec = dj.get_codec("mycodec") -print(f"Name: {codec.name}") -print(f"Internal dtype: {codec.get_dtype(is_external=False)}") -print(f"External dtype: {codec.get_dtype(is_external=True)}") - -# Resolve full chain -from datajoint.codecs import resolve_dtype -final_type, chain, store = resolve_dtype("") -print(f"Final storage type: {final_type}") -print(f"Codec chain: {[c.name for c in chain]}") -print(f"Store: {store}") -``` diff --git a/docs/src/archive/design/tables/codecs.md b/docs/src/archive/design/tables/codecs.md deleted file mode 100644 index ccc9db1f7..000000000 --- a/docs/src/archive/design/tables/codecs.md +++ /dev/null @@ -1,553 +0,0 @@ -# Custom Codecs - -In modern scientific research, data pipelines often involve complex workflows that -generate diverse data types. From high-dimensional imaging data to machine learning -models, these data types frequently exceed the basic representations supported by -traditional relational databases. For example: - -+ A lab working on neural connectivity might use graph objects to represent brain - networks. -+ Researchers processing raw imaging data might store custom objects for pre-processing - configurations. -+ Computational biologists might store fitted machine learning models or parameter - objects for downstream predictions. - -To handle these diverse needs, DataJoint provides the **Codec** system. It -enables researchers to store and retrieve complex, non-standard data types—like Python -objects or data structures—in a relational database while maintaining the -reproducibility, modularity, and query capabilities required for scientific workflows. - -## Overview - -Custom codecs define bidirectional conversion between: - -- **Python objects** (what your code works with) -- **Storage format** (what gets stored in the database) - -``` -┌─────────────────┐ encode() ┌─────────────────┐ -│ Python Object │ ───────────────► │ Storage Type │ -│ (e.g. Graph) │ │ (e.g. bytes) │ -└─────────────────┘ decode() └─────────────────┘ - ◄─────────────── -``` - -## Defining Custom Codecs - -Create a custom codec by subclassing `dj.Codec` and implementing the required -methods. Codecs auto-register when their class is defined: - -```python -import datajoint as dj -import networkx as nx - -class GraphCodec(dj.Codec): - """Custom codec for storing networkx graphs.""" - - # Required: unique identifier used in table definitions - name = "graph" - - def get_dtype(self, is_external: bool) -> str: - """Return the underlying storage type.""" - return "" # Delegate to blob for serialization - - def encode(self, graph, *, key=None, store_name=None): - """Convert graph to storable format (called on INSERT).""" - return { - 'nodes': list(graph.nodes(data=True)), - 'edges': list(graph.edges(data=True)), - } - - def decode(self, stored, *, key=None): - """Convert stored data back to graph (called on FETCH).""" - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - G.add_edges_from(stored['edges']) - return G -``` - -### Required Components - -| Component | Description | -|-----------|-------------| -| `name` | Unique identifier used in table definitions with `` syntax | -| `get_dtype(is_external)` | Returns underlying storage type (e.g., `""`, `"bytes"`, `"json"`) | -| `encode(value, *, key=None, store_name=None)` | Converts Python object to storable format | -| `decode(stored, *, key=None)` | Converts stored data back to Python object | - -### Using Custom Codecs in Tables - -Once defined, use the codec in table definitions with angle brackets: - -```python -@schema -class Connectivity(dj.Manual): - definition = """ - conn_id : int - --- - conn_graph = null : # Uses the GraphCodec we defined - """ -``` - -Insert and fetch work seamlessly: - -```python -import networkx as nx - -# Insert - encode() is called automatically -g = nx.lollipop_graph(4, 2) -Connectivity.insert1({"conn_id": 1, "conn_graph": g}) - -# Fetch - decode() is called automatically -result = (Connectivity & "conn_id = 1").fetch1("conn_graph") -assert isinstance(result, nx.Graph) -``` - -## Auto-Registration - -Codecs automatically register when their class is defined. No decorator needed: - -```python -# This codec is registered automatically when the class is defined -class MyCodec(dj.Codec): - name = "mycodec" - ... -``` - -### Skipping Registration - -For abstract base classes that shouldn't be registered: - -```python -class BaseCodec(dj.Codec, register=False): - """Abstract base - not registered.""" - name = None - -class ConcreteCodec(BaseCodec): - name = "concrete" # This one IS registered - ... -``` - -### Listing Registered Codecs - -```python -# List all registered codec names -print(dj.list_codecs()) -``` - -## Validation - -Add data validation by overriding the `validate()` method. It's called automatically -before `encode()` during INSERT operations: - -```python -class PositiveArrayCodec(dj.Codec): - name = "positive_array" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def validate(self, value): - """Ensure all values are positive.""" - import numpy as np - if not isinstance(value, np.ndarray): - raise TypeError(f"Expected numpy array, got {type(value).__name__}") - if np.any(value < 0): - raise ValueError("Array must contain only positive values") - - def encode(self, array, *, key=None, store_name=None): - return array - - def decode(self, stored, *, key=None): - return stored -``` - -## The `get_dtype()` Method - -The `get_dtype()` method specifies how data is stored. The `is_external` parameter -indicates whether the `@` modifier is present: - -```python -def get_dtype(self, is_external: bool) -> str: - """ - Args: - is_external: True if @ modifier present (e.g., ) - - Returns: - - A core type: "bytes", "json", "varchar(N)", etc. - - Another codec: "", "", etc. - """ -``` - -### Storage Type Options - -| Return Value | Use Case | Database Type | -|--------------|----------|---------------| -| `"bytes"` | Raw binary data | LONGBLOB | -| `"json"` | JSON-serializable data | JSON | -| `"varchar(N)"` | String representations | VARCHAR(N) | -| `"int32"` | Integer identifiers | INT | -| `""` | Serialized Python objects | Depends on internal/external | -| `""` | Large objects with deduplication | JSON (external only) | -| `""` | Chain to another codec | Varies | - -### External Storage - -For large data, use external storage with the `@` modifier: - -```python -class LargeArrayCodec(dj.Codec): - name = "large_array" - - def get_dtype(self, is_external: bool) -> str: - # Use hash-addressed external storage for large data - return "" if is_external else "" - - def encode(self, array, *, key=None, store_name=None): - import pickle - return pickle.dumps(array) - - def decode(self, stored, *, key=None): - import pickle - return pickle.loads(stored) -``` - -Usage: -```python -@schema -class Data(dj.Manual): - definition = ''' - id : int - --- - small_array : # Internal (in database) - big_array : # External (default store) - archive : # External (specific store) - ''' -``` - -## Codec Chaining - -Custom codecs can build on other codecs by returning `` from `get_dtype()`: - -```python -class CompressedGraphCodec(dj.Codec): - name = "compressed_graph" - - def get_dtype(self, is_external: bool) -> str: - return "" # Chain to the GraphCodec - - def encode(self, graph, *, key=None, store_name=None): - # Compress before passing to GraphCodec - return self._compress(graph) - - def decode(self, stored, *, key=None): - # GraphCodec's decode already ran, decompress result - return self._decompress(stored) -``` - -DataJoint automatically resolves the chain to find the final storage type. - -### How Chaining Works - -When DataJoint encounters ``: - -1. `CompressedGraphCodec.get_dtype()` returns `""` -2. `GraphCodec.get_dtype()` returns `""` -3. `BlobCodec.get_dtype()` returns `"bytes"` -4. Final storage type is `bytes` (LONGBLOB in MySQL) - -During INSERT, encoders run outer → inner: -1. `CompressedGraphCodec.encode()` → compressed graph -2. `GraphCodec.encode()` → edge list dict -3. `BlobCodec.encode()` → serialized bytes - -During FETCH, decoders run inner → outer (reverse order). - -## The Key Parameter - -The `key` parameter provides access to primary key values during encode/decode -operations. This is useful when the conversion depends on record context: - -```python -class ContextAwareCodec(dj.Codec): - name = "context_aware" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, value, *, key=None, store_name=None): - if key and key.get("version") == 2: - return self._encode_v2(value) - return self._encode_v1(value) - - def decode(self, stored, *, key=None): - if key and key.get("version") == 2: - return self._decode_v2(stored) - return self._decode_v1(stored) -``` - -## Publishing Codecs as Packages - -Custom codecs can be distributed as installable packages using Python entry points. -This allows codecs to be automatically discovered when the package is installed. - -### Package Structure - -``` -dj-graph-codecs/ -├── pyproject.toml -└── src/ - └── dj_graph_codecs/ - ├── __init__.py - └── codecs.py -``` - -### pyproject.toml - -```toml -[project] -name = "dj-graph-codecs" -version = "1.0.0" -dependencies = ["datajoint>=2.0", "networkx"] - -[project.entry-points."datajoint.codecs"] -graph = "dj_graph_codecs.codecs:GraphCodec" -weighted_graph = "dj_graph_codecs.codecs:WeightedGraphCodec" -``` - -### Codec Implementation - -```python -# src/dj_graph_codecs/codecs.py -import datajoint as dj -import networkx as nx - -class GraphCodec(dj.Codec): - name = "graph" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, graph, *, key=None, store_name=None): - return { - 'nodes': list(graph.nodes(data=True)), - 'edges': list(graph.edges(data=True)), - } - - def decode(self, stored, *, key=None): - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - G.add_edges_from(stored['edges']) - return G - -class WeightedGraphCodec(dj.Codec): - name = "weighted_graph" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, graph, *, key=None, store_name=None): - return [(u, v, d) for u, v, d in graph.edges(data=True)] - - def decode(self, edges, *, key=None): - g = nx.Graph() - for u, v, d in edges: - g.add_edge(u, v, **d) - return g -``` - -### Usage After Installation - -```bash -pip install dj-graph-codecs -``` - -```python -# Codecs are automatically available after package installation -@schema -class MyTable(dj.Manual): - definition = """ - id : int - --- - network : - weighted_network : - """ -``` - -## Complete Example - -Here's a complete example demonstrating custom codecs for a neuroscience workflow: - -```python -import datajoint as dj -import numpy as np - -# Define custom codecs -class SpikeTrainCodec(dj.Codec): - """Efficient storage for sparse spike timing data.""" - name = "spike_train" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def validate(self, value): - if not isinstance(value, np.ndarray): - raise TypeError("Expected numpy array of spike times") - if value.ndim != 1: - raise ValueError("Spike train must be 1-dimensional") - if len(value) > 1 and not np.all(np.diff(value) >= 0): - raise ValueError("Spike times must be sorted") - - def encode(self, spike_times, *, key=None, store_name=None): - # Store as differences (smaller values, better compression) - return np.diff(spike_times, prepend=0).astype(np.float32) - - def decode(self, stored, *, key=None): - # Reconstruct original spike times - return np.cumsum(stored).astype(np.float64) - - -class WaveformCodec(dj.Codec): - """Storage for spike waveform templates with metadata.""" - name = "waveform" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, waveform_dict, *, key=None, store_name=None): - return { - "data": waveform_dict["data"].astype(np.float32), - "sampling_rate": waveform_dict["sampling_rate"], - "channel_ids": list(waveform_dict["channel_ids"]), - } - - def decode(self, stored, *, key=None): - return { - "data": stored["data"].astype(np.float64), - "sampling_rate": stored["sampling_rate"], - "channel_ids": np.array(stored["channel_ids"]), - } - - -# Create schema and tables -schema = dj.schema("ephys_analysis") - -@schema -class Unit(dj.Manual): - definition = """ - unit_id : int - --- - spike_times : - waveform : - quality : enum('good', 'mua', 'noise') - """ - - -# Usage -spike_times = np.array([0.1, 0.15, 0.23, 0.45, 0.67, 0.89]) -waveform = { - "data": np.random.randn(82, 4), - "sampling_rate": 30000, - "channel_ids": [10, 11, 12, 13], -} - -Unit.insert1({ - "unit_id": 1, - "spike_times": spike_times, - "waveform": waveform, - "quality": "good", -}) - -# Fetch - automatically decoded -result = (Unit & "unit_id = 1").fetch1() -print(f"Spike times: {result['spike_times']}") -print(f"Waveform shape: {result['waveform']['data'].shape}") -``` - -## Built-in Codecs - -DataJoint includes several built-in codecs: - -### `` - DataJoint Blob Serialization - -The `` codec provides DataJoint's native binary serialization. It supports: - -- NumPy arrays (compatible with MATLAB) -- Python dicts, lists, tuples, sets -- datetime objects, Decimals, UUIDs -- Nested data structures -- Optional compression - -```python -@schema -class ProcessedData(dj.Manual): - definition = """ - data_id : int - --- - results : # Internal (serialized in database) - large_results : # External (hash-addressed storage) - """ -``` - -### `` - Content-Addressed Storage - -Stores raw bytes using MD5 content hashing with automatic deduplication. -External storage only. - -### `` - Path-Addressed Storage - -Stores files and folders at paths derived from primary keys. Ideal for -Zarr arrays, HDF5 files, and multi-file outputs. External storage only. - -### `` - File Attachments - -Stores files with filename preserved. Supports internal and external storage. - -### `` - File References - -References existing files in configured stores without copying. -External storage only. - -## Best Practices - -1. **Choose descriptive codec names**: Use lowercase with underscores (e.g., `spike_train`, `graph_embedding`) - -2. **Select appropriate storage types**: Use `` for complex objects, `json` for simple structures, `` or `` for large data - -3. **Add validation**: Use `validate()` to catch data errors early - -4. **Document your codecs**: Include docstrings explaining the expected input/output formats - -5. **Handle None values**: Your encode/decode methods may receive `None` for nullable attributes - -6. **Consider versioning**: If your encoding format might change, include version information - -7. **Test round-trips**: Ensure `decode(encode(x)) == x` for all valid inputs - -```python -def test_graph_codec_roundtrip(): - import networkx as nx - g = nx.lollipop_graph(4, 2) - codec = GraphCodec() - - encoded = codec.encode(g) - decoded = codec.decode(encoded) - - assert set(g.edges) == set(decoded.edges) -``` - -## API Reference - -```python -import datajoint as dj - -# List all registered codecs -dj.list_codecs() - -# Get a codec instance -codec = dj.get_codec("blob") -codec = dj.get_codec("") # Angle brackets optional -codec = dj.get_codec("") # Store parameter stripped -``` - -For the complete Codec API specification, see [Codec Specification](codec-spec.md). diff --git a/docs/src/archive/design/tables/declare.md b/docs/src/archive/design/tables/declare.md deleted file mode 100644 index d4fb070a2..000000000 --- a/docs/src/archive/design/tables/declare.md +++ /dev/null @@ -1,242 +0,0 @@ -# Declaration Syntax - -## Creating Tables - -### Classes represent tables - -To make it easy to work with tables in MATLAB and Python, DataJoint programs create a -separate class for each table. -Computer programmers refer to this concept as -[object-relational mapping](https://en.wikipedia.org/wiki/Object-relational_mapping). -For example, the class `experiment.Subject` in the DataJoint client language may -correspond to the table called `subject` on the database server. -Users never need to see the database directly; they only interact with data in the -database by creating and interacting with DataJoint classes. - -#### Data tiers - -The table class must inherit from one of the following superclasses to indicate its -data tier: `dj.Lookup`, `dj.Manual`, `dj.Imported`, `dj.Computed`, or `dj.Part`. -See [tiers](tiers.md) and [master-part](./master-part.md). - -### Defining a table - -To define a DataJoint table in Python: - -1. Define a class inheriting from the appropriate DataJoint class: `dj.Lookup`, -`dj.Manual`, `dj.Imported` or `dj.Computed`. - -2. Decorate the class with the schema object (see [schema](../schema.md)) - -3. Define the class property `definition` to define the table heading. - -For example, the following code defines the table `Person`: - -```python -import datajoint as dj -schema = dj.Schema('alice_experiment') - -@schema -class Person(dj.Manual): - definition = ''' - username : varchar(20) # unique user name - --- - first_name : varchar(30) - last_name : varchar(30) - ''' -``` - -The `@schema` decorator uses the class name and the data tier to check whether an -appropriate table exists on the database. -If a table does not already exist, the decorator creates one on the database using the -definition property. -The decorator attaches the information about the table to the class, and then returns -the class. - -The class will become usable after you define the `definition` property as described in -[Table definition](#table-definition). - -#### DataJoint classes in Python - -DataJoint for Python is implemented through the use of classes providing access to the -actual tables stored on the database. -Since only a single table exists on the database for any class, interactions with all -instances of the class are equivalent. -As such, most methods can be called on the classes themselves rather than on an object, -for convenience. -Whether calling a DataJoint method on a class or on an instance, the result will only -depend on or apply to the corresponding table. -All of the basic functionality of DataJoint is built to operate on the classes -themselves, even when called on an instance. -For example, calling `Person.insert(...)` (on the class) and `Person.insert(...)` (on -an instance) both have the identical effect of inserting data into the table on the -database server. -DataJoint does not prevent a user from working with instances, but the workflow is -complete without the need for instantiation. -It is up to the user whether to implement additional functionality as class methods or -methods called on instances. - -### Valid class names - -Note that in both MATLAB and Python, the class names must follow the CamelCase compound -word notation: - -- start with a capital letter and -- contain only alphanumerical characters (no underscores). - -Examples of valid class names: - -`TwoPhotonScan`, `Scan2P`, `Ephys`, `MembraneVoltage` - -Invalid class names: - -`Two_photon_Scan`, `twoPhotonScan`, `2PhotonScan`, `membranePotential`, `membrane_potential` - -## Table Definition - -DataJoint models data as sets of **entities** with shared **attributes**, often -visualized as tables with rows and columns. -Each row represents a single entity and the values of all of its attributes. -Each column represents a single attribute with a name and a datatype, applicable to -entity in the table. -Unlike rows in a spreadsheet, entities in DataJoint don't have names or numbers: they -can only be identified by the values of their attributes. -Defining a table means defining the names and datatypes of the attributes as well as -the constraints to be applied to those attributes. -Both MATLAB and Python use the same syntax define tables. - -For example, the following code in defines the table `User`, that contains users of the -database: - -The table definition is contained in the `definition` property of the class. - -```python -@schema -class User(dj.Manual): - definition = """ - # database users - username : varchar(20) # unique user name - --- - first_name : varchar(30) - last_name : varchar(30) - role : enum('admin', 'contributor', 'viewer') - """ -``` - -This defines the class `User` that creates the table in the database and provides all -its data manipulation functionality. - -### Table creation on the database server - -Users do not need to do anything special to have a table created in the database. -Tables are created at the time of class definition. -In fact, table creation on the database is one of the jobs performed by the decorator -`@schema` of the class. - -### Changing the definition of an existing table - -Once the table is created in the database, the definition string has no further effect. -In other words, changing the definition string in the class of an existing table will -not actually update the table definition. -To change the table definition, one must first [drop](../drop.md) the existing table. -This means that all the data will be lost, and the new definition will be applied to -create the new empty table. - -Therefore, in the initial phases of designing a DataJoint pipeline, it is common to -experiment with variations of the design before populating it with substantial amounts -of data. - -It is possible to modify a table without dropping it. -This topic is covered separately. - -### Reverse-engineering the table definition - -DataJoint objects provide the `describe` method, which displays the table definition -used to define the table when it was created in the database. -This definition may differ from the definition string of the class if the definition -string has been edited after creation of the table. - -Examples - -```python -s = lab.User.describe() -``` - -## Definition Syntax - -The table definition consists of one or more lines. -Each line can be one of the following: - -- The optional first line starting with a `#` provides a description of the table's purpose. - It may also be thought of as the table's long title. -- A new attribute definition in any of the following forms (see -[Attributes](./attributes.md) for valid datatypes): - ``name : datatype`` - ``name : datatype # comment`` - ``name = default : datatype`` - ``name = default : datatype # comment`` -- The divider `---` (at least three hyphens) separating primary key attributes above -from secondary attributes below. -- A foreign key in the format `-> ReferencedTable`. - (See [Dependencies](dependencies.md).) - -For example, the table for Persons may have the following definition: - -```python -# Persons in the lab -username : varchar(16) # username in the database ---- -full_name : varchar(255) -start_date : date # date when joined the lab -``` - -This will define the table with attributes `username`, `full_name`, and `start_date`, -in which `username` is the [primary key](primary.md). - -### Attribute names - -Attribute names must be in lowercase and must start with a letter. -They can only contain alphanumerical characters and underscores. -The attribute name cannot exceed 64 characters. - -Valid attribute names - `first_name`, `two_photon_scan`, `scan_2p`, `two_photon_scan` - -Invalid attribute names - `firstName`, `first name`, `2photon_scan`, `two-photon_scan`, `TwoPhotonScan` - -Ideally, attribute names should be unique across all tables that are likely to be used -in queries together. -For example, tables often have attributes representing the start times of sessions, -recordings, etc. -Such attributes must be uniquely named in each table, such as `session_start_time` or -`recording_start_time`. - -### Default values - -Secondary attributes can be given default values. -A default value will be used for an attribute if no other value is given at the time -the entity is [inserted](../../manipulation/insert.md) into the table. -Generally, default values are numerical values or character strings. -Default values for dates must be given as strings as well, contained within quotes -(with the exception of `CURRENT_TIMESTAMP`). -Note that default values can only be used when inserting as a mapping. -Primary key attributes cannot have default values (with the exceptions of -`auto_increment` and `CURRENT_TIMESTAMP` attributes; see [primary-key](primary.md)). - -An attribute with a default value of `NULL` is called a **nullable attribute**. -A nullable attribute can be thought of as applying to all entities in a table but -having an optional *value* that may be absent in some entities. -Nullable attributes should *not* be used to indicate that an attribute is inapplicable -to some entities in a table (see [normalization](../normalization.md)). -Nullable attributes should be used sparingly to indicate optional rather than -inapplicable attributes that still apply to all entities in the table. -`NULL` is a special literal value and does not need to be enclosed in quotes. - -Here are some examples of attributes with default values: - -```python -failures = 0 : int -due_date = "2020-05-31" : date -additional_comments = NULL : varchar(256) -``` diff --git a/docs/src/archive/design/tables/dependencies.md b/docs/src/archive/design/tables/dependencies.md deleted file mode 100644 index e06278ee8..000000000 --- a/docs/src/archive/design/tables/dependencies.md +++ /dev/null @@ -1,241 +0,0 @@ -# Dependencies - -## Understanding dependencies - -A schema contains collections of tables of related data. -Accordingly, entities in one table often derive some of their meaning or context from -entities in other tables. -A **foreign key** defines a **dependency** of entities in one table on entities in -another within a schema. -In more complex designs, dependencies can even exist between entities in tables from -different schemas. -Dependencies play a functional role in DataJoint and do not simply label the structure -of a pipeline. -Dependencies provide entities in one table with access to data in another table and -establish certain constraints on entities containing a foreign key. - -A DataJoint pipeline, including the dependency relationships established by foreign -keys, can be visualized as a graph with nodes and edges. -The diagram of such a graph is called the **entity relationship diagram** or -[Diagram](../diagrams.md). -The nodes of the graph are tables and the edges connecting them are foreign keys. -The edges are directed and the overall graph is a **directed acyclic graph**, a graph -with no loops. - -For example, the Diagram below is the pipeline for multipatching experiments - -![mp-diagram](../../images/mp-diagram.png){: style="align:center"} - -The graph defines the direction of the workflow. -The tables at the top of the flow need to be populated first, followed by those tables -one step below and so forth until the last table is populated at the bottom of the -pipeline. -The top of the pipeline tends to be dominated by lookup tables (gray stars) and manual -tables (green squares). -The middle has many imported tables (blue triangles), and the bottom has computed -tables (red stars). - -## Defining a dependency - -Foreign keys are defined with arrows `->` in the [table definition](declare.md), -pointing to another table. - -A foreign key may be defined as part of the [primary-key](primary.md). - -In the Diagram, foreign keys from the primary key are shown as solid lines. -This means that the primary key of the referenced table becomes part of the primary key -of the new table. -A foreign key outside the primary key is indicated by dashed line in the ERD. - -For example, the following definition for the table `mp.Slice` has three foreign keys, -including one within the primary key. - -```python -# brain slice --> mp.Subject -slice_id : smallint # slice number within subject ---- --> mp.BrainRegion --> mp.Plane -slice_date : date # date of the slicing (not patching) -thickness : smallint unsigned # slice thickness in microns -experimenter : varchar(20) # person who performed this experiment -``` - -You can examine the resulting table heading with - -```python -mp.BrainSlice.heading -``` - -The heading of `mp.Slice` may look something like - -```python -subject_id : char(8) # experiment subject id -slice_id : smallint # slice number within subject ---- -brain_region : varchar(12) # abbreviated name for brain region -plane : varchar(12) # plane of section -slice_date : date # date of the slicing (not patching) -thickness : smallint unsigned # slice thickness in microns -experimenter : varchar(20) # person who performed this experiment -``` - -This displayed heading reflects the actual attributes in the table. -The foreign keys have been replaced by the primary key attributes of the referenced -tables, including their data types and comments. - -## How dependencies work - -The foreign key `-> A` in the definition of table `B` has the following effects: - -1. The primary key attributes of `A` are made part of `B`'s definition. -2. A referential constraint is created in `B` with reference to `A`. -3. If one does not already exist, an index is created to speed up searches in `B` for -matches to `A`. - (The reverse search is already fast because it uses the primary key of `A`.) - -A referential constraint means that an entity in `B` cannot exist without a matching -entity in `A`. -**Matching** means attributes in `B` that correspond to the primary key of `A` must -have the same values. -An attempt to insert an entity into `B` that does not have a matching counterpart in -`A` will fail. -Conversely, deleting an entity from `A` that has matching entities in `B` will result -in the deletion of those matching entities and so forth, recursively, downstream in the -pipeline. - -When `B` references `A` with a foreign key, one can say that `B` **depends** on `A`. -In DataJoint terms, `B` is the **dependent table** and `A` is the **referenced table** -with respect to the foreign key from `B` to `A`. - -Note to those already familiar with the theory of relational databases: The usage of -the words "depends" and "dependency" here should not be confused with the unrelated -concept of *functional dependencies* that is used to define normal forms. - -## Referential integrity - -Dependencies enforce the desired property of databases known as -**referential integrity**. -Referential integrity is the guarantee made by the data management process that related -data across the database remain present, correctly associated, and mutually consistent. -Guaranteeing referential integrity means enforcing the constraint that no entity can -exist in the database without all the other entities on which it depends. -An entity in table `B` depends on an entity in table `A` when they belong to them or -are computed from them. - -## Dependencies with renamed attributes - -In most cases, a dependency includes the primary key attributes of the referenced table -as they appear in its table definition. -Sometimes it can be helpful to choose a new name for a foreign key attribute that -better fits the context of the dependent table. -DataJoint provides the following [projection](../../query/project.md) syntax to rename -the primary key attributes when they are included in the new table. - -The dependency - -```python --> Table.project(new_attr='old_attr') -``` - -renames the primary key attribute `old_attr` of `Table` as `new_attr` before -integrating it into the table definition. -Any additional primary key attributes will retain their original names. -For example, the table `Experiment` may depend on table `User` but rename the `user` -attribute into `operator` as follows: - -```python --> User.proj(operator='user') -``` - -In the above example, an entity in the dependent table depends on exactly one entity in -the referenced table. -Sometimes entities may depend on multiple entities from the same table. -Such a design requires a way to distinguish between dependent attributes having the -same name in the reference table. -For example, a table for `Synapse` may reference the table `Cell` twice as -`presynaptic` and `postsynaptic`. -The table definition may appear as - -```python -# synapse between two cells --> Cell.proj(presynaptic='cell_id') --> Cell.proj(postsynaptic='cell_id') ---- -connection_strength : double # (pA) peak synaptic current -``` - -If the primary key of `Cell` is (`animal_id`, `slice_id`, `cell_id`), then the primary -key of `Synapse` resulting from the above definition will be (`animal_id`, `slice_id`, -`presynaptic`, `postsynaptic`). -Projection always returns all of the primary key attributes of a table, so `animal_id` -and `slice_id` are included, with their original names. - -Note that the design of the `Synapse` table above imposes the constraint that the -synapse can only be found between cells in the same animal and in the same slice. - -Allowing representation of synapses between cells from different slices requires the -renamimg of `slice_id` as well: - -```python -# synapse between two cells --> Cell(presynaptic_slice='slice_id', presynaptic_cell='cell_id') --> Cell(postsynaptic_slice='slice_id', postsynaptic_cell='cell_id') ---- -connection_strength : double # (pA) peak synaptic current -``` - -In this case, the primary key of `Synapse` will be (`animal_id`, `presynaptic_slice`, -`presynaptic_cell`, `postsynaptic_slice`, `postsynaptic_cell`). -This primary key still imposes the constraint that synapses can only form between cells -within the same animal but now allows connecting cells across different slices. - -In the Diagram, renamed foreign keys are shown as red lines with an additional dot node -in the middle to indicate that a renaming took place. - -## Foreign key options - -Note: Foreign key options are currently in development. - -Foreign keys allow the additional options `nullable` and `unique`, which can be -inserted in square brackets following the arrow. - -For example, in the following table definition - -```python -rig_id : char(4) # experimental rig ---- --> Person -``` - -each rig belongs to a person, but the table definition does not prevent one person -owning multiple rigs. -With the `unique` option, a person may only appear once in the entire table, which -means that no one person can own more than one rig. - -```python -rig_id : char(4) # experimental rig ---- --> [unique] Person -``` - -With the `nullable` option, a rig may not belong to anyone, in which case the foreign -key attributes for `Person` are set to `NULL`: - -```python -rig_id : char(4) # experimental rig ---- --> [nullable] Person -``` - -Finally with both `unique` and `nullable`, a rig may or may not be owned by anyone and -each person may own up to one rig. - -```python -rig_id : char(4) # experimental rig ---- --> [unique, nullable] Person -``` - -Foreign keys made from the primary key cannot be nullable but may be unique. diff --git a/docs/src/archive/design/tables/filepath.md b/docs/src/archive/design/tables/filepath.md deleted file mode 100644 index 05e9ca744..000000000 --- a/docs/src/archive/design/tables/filepath.md +++ /dev/null @@ -1,96 +0,0 @@ -# Filepath Datatype - -Note: Filepath Datatype is available as a preview feature in DataJoint Python v0.12. -This means that the feature is required to be explicitly enabled. To do so, make sure -to set the environment variable `FILEPATH_FEATURE_SWITCH=TRUE` prior to use. - -## Configuration & Usage - -Corresponding to issue -[#481](https://github.com/datajoint/datajoint-python/issues/481), -the `filepath` attribute type links DataJoint records to files already -managed outside of DataJoint. This can aid in sharing data with -other systems such as allowing an image viewer application to -directly use files from a DataJoint pipeline, or to allow downstream -tables to reference data which reside outside of DataJoint -pipelines. - -To define a table using the `filepath` datatype, an existing DataJoint -[store](../../sysadmin/external-store.md) should be created and then referenced in the -new table definition. For example, given a simple store: - -```python - dj.config['stores'] = { - 'data': { - 'protocol': 'file', - 'location': '/data', - 'stage': '/data' - } - } -``` - -we can define an `ScanImages` table as follows: - -```python -@schema -class ScanImages(dj.Manual): - definition = """ - -> Session - image_id: int - --- - image_path: filepath@data - """ -``` - -This table can now be used for tracking paths within the `/data` local directory. -For example: - -```python ->>> ScanImages.insert1((0, 0, '/data/images/image_0.tif')) ->>> (ScanImages() & {'session_id': 0}).fetch1(as_dict=True) -{'session_id': 0, 'image_id': 0, 'image_path': '/data/images/image_0.tif'} -``` - -As can be seen from the example, unlike [blob](blobs.md) records, file -paths are managed as path locations to the underlying file. - -## Integrity Notes - -Unlike other data in DataJoint, data in `filepath` records are -deliberately intended for shared use outside of DataJoint. To help -ensure integrity of `filepath` records, DataJoint will record a -checksum of the file data on `insert`, and will verify this checksum -on `fetch`. However, since the underlying file data may be shared -with other applications, special care should be taken to ensure -records stored in `filepath` attributes are not modified outside -of the pipeline, or, if they are, that records in the pipeline are -updated accordingly. A safe method of changing `filepath` data is -as follows: - -1. Delete the `filepath` database record. - This will ensure that any downstream records in the pipeline depending - on the `filepath` record are purged from the database. -2. Modify `filepath` data. -3. Re-insert corresponding the `filepath` record. - This will add the record back to DataJoint with an updated file checksum. -4. Compute any downstream dependencies, if needed. - This will ensure that downstream results dependent on the `filepath` - record are updated to reflect the newer `filepath` contents. - -### Disable Fetch Verification - -Note: Skipping the checksum is not recommended as it ensures file integrity i.e. -downloaded files are not corrupted. With S3 stores, most of the time to complete a -`.fetch()` is from the file download itself as opposed to evaluating the checksum. This -option will primarily benefit `filepath` usage connected to a local `file` store. - -To disable checksums you can set a threshold in bytes -for when to stop evaluating checksums like in the example below: - -```python -dj.config["filepath_checksum_size_limit"] = 5 * 1024**3 # Skip for all files greater than 5GiB -``` - -The default is `None` which means it will always verify checksums. - - diff --git a/docs/src/archive/design/tables/indexes.md b/docs/src/archive/design/tables/indexes.md deleted file mode 100644 index 9d8148c36..000000000 --- a/docs/src/archive/design/tables/indexes.md +++ /dev/null @@ -1,97 +0,0 @@ -# Indexes - -Table indexes are data structures that allow fast lookups by an indexed attribute or -combination of attributes. - -In DataJoint, indexes are created by one of the three mechanisms: - -1. Primary key -2. Foreign key -3. Explicitly defined indexes - -The first two mechanisms are obligatory. Every table has a primary key, which serves as -an unique index. Therefore, restrictions by a primary key are very fast. Foreign keys -create additional indexes unless a suitable index already exists. - -## Indexes for single primary key tables - -Let’s say a mouse in the lab has a lab-specific ID but it also has a separate id issued -by the animal facility. - -```python -@schema -class Mouse(dj.Manual): - definition = """ - mouse_id : int # lab-specific ID - --- - tag_id : int # animal facility ID - """ -``` - -In this case, searching for a mouse by `mouse_id` is much faster than by `tag_id` -because `mouse_id` is a primary key, and is therefore indexed. - -To make searches faster on fields other than the primary key or a foreign key, you can -add a secondary index explicitly. - -Regular indexes are declared as `index(attr1, ..., attrN)` on a separate line anywhere in -the table declaration (below the primary key divide). - -Indexes can be declared with unique constraint as `unique index (attr1, ..., attrN)`. - -Let’s redeclare the table with a unique index on `tag_id`. - -```python -@schema -class Mouse(dj.Manual): - definition = """ - mouse_id : int # lab-specific ID - --- - tag_id : int # animal facility ID - unique index (tag_id) - """ -``` -Now, searches with `mouse_id` and `tag_id` are similarly fast. - -## Indexes for tables with multiple primary keys - -Let’s now imagine that rats in a lab are identified by the combination of `lab_name` and -`rat_id` in a table `Rat`. - -```python -@schema -class Rat(dj.Manual): - definition = """ - lab_name : char(16) - rat_id : int unsigned # lab-specific ID - --- - date_of_birth = null : date - """ -``` -Note that despite the fact that `rat_id` is in the index, searches by `rat_id` alone are not -helped by the index because it is not first in the index. This is similar to searching for -a word in a dictionary that orders words alphabetically. Searching by the first letters -of a word is easy but searching by the last few letters of a word requires scanning the -whole dictionary. - -In this table, the primary key is a unique index on the combination `(lab_name, rat_id)`. -Therefore searches on these attributes or on `lab_name` alone are fast. But this index -cannot help searches on `rat_id` alone. Similarly, searing by `date_of_birth` requires a -full-table scan and is inefficient. - -To speed up searches by the `rat_id` and `date_of_birth`, we can explicit indexes to -`Rat`: - -```python -@schema -class Rat2(dj.Manual): - definition = """ - lab_name : char(16) - rat_id : int unsigned # lab-specific ID - --- - date_of_birth = null : date - - index(rat_id) - index(date_of_birth) - """ -``` diff --git a/docs/src/archive/design/tables/lookup.md b/docs/src/archive/design/tables/lookup.md deleted file mode 100644 index 79b2c67ba..000000000 --- a/docs/src/archive/design/tables/lookup.md +++ /dev/null @@ -1,31 +0,0 @@ -# Lookup Tables - -Lookup tables contain basic facts that are not specific to an experiment and are fairly -persistent. -Their contents are typically small. -In GUIs, lookup tables are often used for drop-down menus or radio buttons. -In computed tables, they are often used to specify alternative methods for computations. -Lookup tables are commonly populated from their `contents` property. -In a [diagram](../diagrams.md) they are shown in gray. -The decision of which tables are lookup tables and which are manual can be somewhat -arbitrary. - -The table below is declared as a lookup table with its contents property provided to -generate entities. - -```python -@schema -class User(dj.Lookup): - definition = """ - # users in the lab - username : varchar(20) # user in the lab - --- - first_name : varchar(20) # user first name - last_name : varchar(20) # user last name - """ - contents = [ - ['cajal', 'Santiago', 'Cajal'], - ['hubel', 'David', 'Hubel'], - ['wiesel', 'Torsten', 'Wiesel'] - ] -``` diff --git a/docs/src/archive/design/tables/manual.md b/docs/src/archive/design/tables/manual.md deleted file mode 100644 index d97b6ce52..000000000 --- a/docs/src/archive/design/tables/manual.md +++ /dev/null @@ -1,47 +0,0 @@ -# Manual Tables - -Manual tables are populated during experiments through a variety of interfaces. -Not all manual information is entered by typing. -Automated software can enter it directly into the database. -What makes a manual table manual is that it does not perform any computations within -the DataJoint pipeline. - -The following code defines three manual tables `Animal`, `Session`, and `Scan`: - -```python -@schema -class Animal(dj.Manual): - definition = """ - # information about animal - animal_id : int # animal id assigned by the lab - --- - -> Species - date_of_birth=null : date # YYYY-MM-DD optional - sex='' : enum('M', 'F', '') # leave empty if unspecified - """ - -@schema -class Session(dj.Manual): - definition = """ - # Experiment Session - -> Animal - session : smallint # session number for the animal - --- - session_date : date # YYYY-MM-DD - -> User - -> Anesthesia - -> Rig - """ - -@schema -class Scan(dj.Manual): - definition = """ - # Two-photon imaging scan - -> Session - scan : smallint # scan number within the session - --- - -> Lens - laser_wavelength : decimal(5,1) # um - laser_power : decimal(4,1) # mW - """ -``` diff --git a/docs/src/archive/design/tables/master-part.md b/docs/src/archive/design/tables/master-part.md deleted file mode 100644 index d0f575e4d..000000000 --- a/docs/src/archive/design/tables/master-part.md +++ /dev/null @@ -1,112 +0,0 @@ -# Master-Part Relationship - -Often an entity in one table is inseparably associated with a group of entities in -another, forming a **master-part** relationship. -The master-part relationship ensures that all parts of a complex representation appear -together or not at all. -This has become one of the most powerful data integrity principles in DataJoint. - -As an example, imagine segmenting an image to identify regions of interest. -The resulting segmentation is inseparable from the ROIs that it produces. -In this case, the two tables might be called `Segmentation` and `Segmentation.ROI`. - -In Python, the master-part relationship is expressed by making the part a nested class -of the master. -The part is subclassed from `dj.Part` and does not need the `@schema` decorator. - -```python -@schema -class Segmentation(dj.Computed): - definition = """ # image segmentation - -> Image - """ - - class ROI(dj.Part): - definition = """ # Region of interest resulting from segmentation - -> Segmentation - roi : smallint # roi number - --- - roi_pixels : # indices of pixels - roi_weights : # weights of pixels - """ - - def make(self, key): - image = (Image & key).fetch1('image') - self.insert1(key) - count = itertools.count() - Segmentation.ROI.insert( - dict(key, roi=next(count), roi_pixel=roi_pixels, roi_weights=roi_weights) - for roi_pixels, roi_weights in mylib.segment(image)) -``` - -## Populating - -Master-part relationships can form in any data tier, but DataJoint observes them more -strictly for auto-populated tables. -To populate both the master `Segmentation` and the part `Segmentation.ROI`, it is -sufficient to call the `populate` method of the master: - -```python -Segmentation.populate() -``` - -Note that the entities in the master and the matching entities in the part are inserted -within a single `make` call of the master, which means that they are a processed inside -a single transactions: either all are inserted and committed or the entire transaction -is rolled back. -This ensures that partial results never appear in the database. - -For example, imagine that a segmentation is performed, but an error occurs halfway -through inserting the results. -If this situation were allowed to persist, then it might appear that 20 ROIs were -detected where 45 had actually been found. - -## Deleting - -To delete from a master-part pair, one should never delete from the part tables -directly. -The only valid method to delete from a part table is to delete the master. -This has been an unenforced rule, but upcoming versions of DataJoint will prohibit -direct deletes from the master table. -DataJoint's [delete](../../manipulation/delete.md) operation is also enclosed in a -transaction. - -Together, the rules of master-part relationships ensure a key aspect of data integrity: -results of computations involving multiple components and steps appear in their -entirety or not at all. - -## Multiple parts - -The master-part relationship cannot be chained or nested. -DataJoint does not allow part tables of other part tables per se. -However, it is common to have a master table with multiple part tables that depend on -each other. -For example: - -```python -@schema -class ArrayResponse(dj.Computed): -definition = """ -array: int -""" - -class ElectrodeResponse(dj.Part): -definition = """ --> master -electrode: int # electrode number on the probe -""" - -class ChannelResponse(dj.Part): -definition = """ --> ElectrodeResponse -channel: int ---- -response: # response of a channel -""" -``` - -Conceptually, one or more channels belongs to an electrode, and one or more electrodes -belong to an array. -This example assumes that information about an array's response (which consists -ultimately of the responses of multiple electrodes each consisting of multiple channel -responses) including it's electrodes and channels are entered together. diff --git a/docs/src/archive/design/tables/object.md b/docs/src/archive/design/tables/object.md deleted file mode 100644 index e2ed8bf25..000000000 --- a/docs/src/archive/design/tables/object.md +++ /dev/null @@ -1,357 +0,0 @@ -# Object Type - -The `object` type provides managed file and folder storage for DataJoint pipelines. Unlike `attach@store` and `filepath@store` which reference named stores, the `object` type uses a unified storage backend configured at the pipeline level. - -## Overview - -The `object` type supports both files and folders: - -- **Files**: Copied to storage at insert time, accessed via handle on fetch -- **Folders**: Entire directory trees stored as a unit (e.g., Zarr arrays) -- **Staged inserts**: Write directly to storage for large objects - -### Key Features - -- **Unified storage**: One storage backend per pipeline (local filesystem or cloud) -- **No hidden tables**: Metadata stored inline as JSON (simpler than `attach@store`) -- **fsspec integration**: Direct access for Zarr, xarray, and other array libraries -- **Immutable objects**: Content cannot be modified after insert - -## Configuration - -Configure object storage in `datajoint.json`: - -```json -{ - "object_storage": { - "project_name": "my_project", - "protocol": "s3", - "bucket": "my-bucket", - "location": "my_project", - "endpoint": "s3.amazonaws.com" - } -} -``` - -For local filesystem storage: - -```json -{ - "object_storage": { - "project_name": "my_project", - "protocol": "file", - "location": "/data/my_project" - } -} -``` - -### Configuration Options - -| Setting | Required | Description | -|---------|----------|-------------| -| `project_name` | Yes | Unique project identifier | -| `protocol` | Yes | Storage backend: `file`, `s3`, `gcs`, `azure` | -| `location` | Yes | Base path or bucket prefix | -| `bucket` | For cloud | Bucket name (S3, GCS, Azure) | -| `endpoint` | For S3 | S3 endpoint URL | -| `partition_pattern` | No | Path pattern with `{attribute}` placeholders | -| `token_length` | No | Random suffix length (default: 8, range: 4-16) | - -### Environment Variables - -Settings can be overridden via environment variables: - -```bash -DJ_OBJECT_STORAGE_PROTOCOL=s3 -DJ_OBJECT_STORAGE_BUCKET=my-bucket -DJ_OBJECT_STORAGE_LOCATION=my_project -``` - -## Table Definition - -Define an object attribute in your table: - -```python -@schema -class Recording(dj.Manual): - definition = """ - subject_id : int - session_id : int - --- - raw_data : object # managed file storage - processed : object # another object attribute - """ -``` - -Note: No `@store` suffix needed—storage is determined by pipeline configuration. - -## Insert Operations - -### Inserting Files - -Insert a file by providing its local path: - -```python -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "/local/path/to/recording.dat" -}) -``` - -The file is copied to object storage and the path is stored as JSON metadata. - -### Inserting Folders - -Insert an entire directory: - -```python -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "/local/path/to/data_folder/" -}) -``` - -### Inserting from Remote URLs - -Insert from cloud storage or HTTP sources—content is copied to managed storage: - -```python -# From S3 -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "s3://source-bucket/path/to/data.dat" -}) - -# From Google Cloud Storage (e.g., collaborator data) -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "neural_data": "gs://collaborator-bucket/shared/experiment.zarr" -}) - -# From HTTP/HTTPS -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "https://example.com/public/data.dat" -}) -``` - -Supported protocols: `s3://`, `gs://`, `az://`, `http://`, `https://` - -Remote sources may require credentials configured via environment variables or fsspec configuration files. - -### Inserting from Streams - -Insert from a file-like object with explicit extension: - -```python -with open("/local/path/data.bin", "rb") as f: - Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": (".bin", f) - }) -``` - -### Staged Insert (Direct Write) - -For large objects like Zarr arrays, use staged insert to write directly to storage without a local copy: - -```python -import zarr - -with Recording.staged_insert1 as staged: - # Set primary key values first - staged.rec['subject_id'] = 123 - staged.rec['session_id'] = 45 - - # Create Zarr array directly in object storage - z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(10000, 10000)) - z[:] = compute_large_array() - - # Assign to record - staged.rec['raw_data'] = z - -# On successful exit: metadata computed, record inserted -# On exception: storage cleaned up, no record inserted -``` - -The `staged_insert1` context manager provides: - -- `staged.rec`: Dict for setting attribute values -- `staged.store(field, ext)`: Returns `fsspec.FSMap` for Zarr/xarray -- `staged.open(field, ext, mode)`: Returns file handle for writing -- `staged.fs`: Direct fsspec filesystem access - -## Fetch Operations - -Fetching an object attribute returns an `ObjectRef` handle: - -```python -record = Recording.fetch1() -obj = record["raw_data"] - -# Access metadata (no I/O) -print(obj.path) # Storage path -print(obj.size) # Size in bytes -print(obj.ext) # File extension (e.g., ".dat") -print(obj.is_dir) # True if folder -``` - -### Reading File Content - -```python -# Read entire file as bytes -content = obj.read() - -# Open as file object -with obj.open() as f: - data = f.read() -``` - -### Working with Folders - -```python -# List contents -contents = obj.listdir() - -# Walk directory tree -for root, dirs, files in obj.walk(): - print(root, files) - -# Open specific file in folder -with obj.open("subdir/file.dat") as f: - data = f.read() -``` - -### Downloading Files - -Download to local filesystem: - -```python -# Download entire object -local_path = obj.download("/local/destination/") - -# Download specific file from folder -local_path = obj.download("/local/destination/", "subdir/file.dat") -``` - -### Integration with Zarr and xarray - -The `ObjectRef` provides direct fsspec access: - -```python -import zarr -import xarray as xr - -record = Recording.fetch1() -obj = record["raw_data"] - -# Open as Zarr array -z = zarr.open(obj.store, mode='r') -print(z.shape) - -# Open with xarray -ds = xr.open_zarr(obj.store) - -# Access fsspec filesystem directly -fs = obj.fs -files = fs.ls(obj.full_path) -``` - -### Verifying Integrity - -Verify that stored content matches metadata: - -```python -try: - obj.verify() - print("Object integrity verified") -except IntegrityError as e: - print(f"Verification failed: {e}") -``` - -For files, this checks size (and hash if available). For folders, it validates the manifest. - -## Storage Structure - -Objects are stored with a deterministic path structure: - -``` -{location}/{schema}/{Table}/objects/{pk_attrs}/{field}_{token}{ext} -``` - -Example: -``` -my_project/my_schema/Recording/objects/subject_id=123/session_id=45/raw_data_Ax7bQ2kM.dat -``` - -### Partitioning - -Use `partition_pattern` to organize files by attributes: - -```json -{ - "object_storage": { - "partition_pattern": "{subject_id}/{session_id}" - } -} -``` - -This promotes specified attributes to the path root for better organization: - -``` -my_project/subject_id=123/session_id=45/my_schema/Recording/objects/raw_data_Ax7bQ2kM.dat -``` - -## Database Storage - -The `object` type is stored as a JSON column containing metadata: - -```json -{ - "path": "my_schema/Recording/objects/subject_id=123/raw_data_Ax7bQ2kM.dat", - "size": 12345, - "hash": null, - "ext": ".dat", - "is_dir": false, - "timestamp": "2025-01-15T10:30:00Z", - "mime_type": "application/octet-stream" -} -``` - -For folders, the metadata includes `item_count` and a manifest file is stored alongside the folder in object storage. - -## Comparison with Other Types - -| Feature | `attach@store` | `filepath@store` | `object` | -|---------|----------------|------------------|----------| -| Store config | Per-attribute | Per-attribute | Per-pipeline | -| Path control | DataJoint | User-managed | DataJoint | -| Hidden tables | Yes | Yes | **No** | -| Backend | File/S3 only | File/S3 only | fsspec (any) | -| Metadata storage | External table | External table | Inline JSON | -| Folder support | No | No | **Yes** | -| Direct write | No | No | **Yes** | - -## Delete Behavior - -When a record is deleted: - -1. Database record is deleted first (within transaction) -2. Storage file/folder deletion is attempted after commit -3. File deletion failures are logged but don't fail the transaction - -Orphaned files (from failed deletes or crashed inserts) can be cleaned up using maintenance utilities. - -## Best Practices - -1. **Use staged insert for large objects**: Avoid copying multi-gigabyte files through local storage -2. **Set primary keys before calling `store()`**: The storage path depends on primary key values -3. **Use meaningful extensions**: Extensions like `.zarr`, `.hdf5` help identify content type -4. **Verify after critical inserts**: Call `obj.verify()` for important data -5. **Configure partitioning for large datasets**: Improves storage organization and browsing diff --git a/docs/src/archive/design/tables/primary.md b/docs/src/archive/design/tables/primary.md deleted file mode 100644 index fc4f5b8e0..000000000 --- a/docs/src/archive/design/tables/primary.md +++ /dev/null @@ -1,178 +0,0 @@ -# Primary Key - -## Primary keys in DataJoint - -Entities in tables are neither named nor numbered. -DataJoint does not answer questions of the type "What is the 10th element of this table?" -Instead, entities are distinguished by the values of their attributes. -Furthermore, the entire entity is not required for identification. -In each table, a subset of its attributes are designated to be the **primary key**. -Attributes in the primary key alone are sufficient to differentiate any entity from any -other within the table. - -Each table must have exactly one -[primary key](http://en.wikipedia.org/wiki/Primary_key): a subset of its attributes -that uniquely identify each entity in the table. -The database uses the primary key to prevent duplicate entries, to relate data across -tables, and to accelerate data queries. -The choice of the primary key will determine how you identify entities. -Therefore, make the primary key **short**, **expressive**, and **persistent**. - -For example, mice in our lab are assigned unique IDs. -The mouse ID number `animal_id` of type `smallint` can serve as the primary key for the -table `Mice`. -An experiment performed on a mouse may be identified in the table `Experiments` by two -attributes: `animal_id` and `experiment_number`. - -DataJoint takes the concept of primary keys somewhat more seriously than other models -and query languages. -Even **table expressions**, i.e. those tables produced through operations on other -tables, have a well-defined primary key. -All operators on tables are designed in such a way that the results always have a -well-defined primary key. - -In all representations of tables in DataJoint, the primary key attributes are always -listed before other attributes and highlighted for emphasis (e.g. in a **bold** font or -marked with an asterisk \*) - -## Defining a primary key - -In table declarations, the primary key attributes always come first and are separated -from the other attributes with a line containing at least three hyphens. -For example, the following is the definition of a table containing database users where -`username` is the primary key. - -```python -# database users -username : varchar(20) # unique user name ---- -first_name : varchar(30) -last_name : varchar(30) -role : enum('admin', 'contributor', 'viewer') -``` - -## Entity integrity - -The primary key defines and enforces the desired property of databases known as -[entity integrity](../integrity.md). -**Entity integrity** ensures that there is a one-to-one and unambiguous mapping between -real-world entities and their representations in the database system. -The data management process must prevent any duplication or misidentification of -entities. - -To enforce entity integrity, DataJoint implements several rules: - -- Every table must have a primary key. -- Primary key attributes cannot have default values (with the exception of -`auto_increment` and `CURRENT_TIMESTAMP`; see below). -- Operators on tables are defined with respect to the primary key and preserve a -primary key in their results. - -## Datatypes in primary keys - -All integer types, dates, timestamps, and short character strings make good primary key -attributes. -Character strings are somewhat less suitable because they can be long and because they -may have invisible trailing spaces. -Floating-point numbers should be avoided because rounding errors may lead to -misidentification of entities. -Enums are okay as long as they do not need to be modified after -[dependencies](dependencies.md) are already created referencing the table. -Finally, DataJoint does not support blob types in primary keys. - -The primary key may be composite, i.e. comprising several attributes. -In DataJoint, hierarchical designs often produce tables whose primary keys comprise -many attributes. - -## Choosing primary key attributes - -A primary key comprising real-world attributes is a good choice when such real-world -attributes are already properly and permanently assigned. -Whatever characteristics are used to uniquely identify the actual entities can be used -to identify their representations in the database. - -If there are no attributes that could readily serve as a primary key, an artificial -attribute may be created solely for the purpose of distinguishing entities. -In such cases, the primary key created for management in the database must also be used -to uniquely identify the entities themselves. -If the primary key resides only in the database while entities remain indistinguishable -in the real world, then the process cannot ensure entity integrity. -When a primary key is created as part of data management rather than based on -real-world attributes, an institutional process must ensure the uniqueness and -permanence of such an identifier. - -For example, the U.S. government assigns every worker an identifying attribute, the -social security number. -However, the government must go to great lengths to ensure that this primary key is -assigned exactly once, by checking against other less convenient candidate keys (i.e. -the combination of name, parents' names, date of birth, place of birth, etc.). -Just like the SSN, well managed primary keys tend to get institutionalized and find -multiple uses. - -Your lab must maintain a system for uniquely identifying important entities. -For example, experiment subjects and experiment protocols must have unique IDs. -Use these as the primary keys in the corresponding tables in your DataJoint databases. - -### Using hashes as primary keys - -Some tables include too many attributes in their primary keys. -For example, the stimulus condition in a psychophysics experiment may have a dozen -parameters such that a change in any one of them makes a different valid stimulus -condition. -In such a case, all the attributes would need to be included in the primary key to -ensure entity integrity. -However, long primary keys make it difficult to reference individual entities. -To be most useful, primary keys need to be relatively short. - -This problem is effectively solved through the use of a hash of all the identifying -attributes as the primary key. -For example, MD5 or SHA-1 hash algorithms can be used for this purpose. -To keep their representations human-readable, they may be encoded in base-64 ASCII. -For example, the 128-bit MD5 hash can be represented by 21 base-64 ASCII characters, -but for many applications, taking the first 8 to 12 characters is sufficient to avoid -collisions. - -### `auto_increment` - -Some entities are created by the very action of being entered into the database. -The action of entering them into the database gives them their identity. -It is impossible to duplicate them since entering the same thing twice still means -creating two distinct entities. - -In such cases, the use of an auto-incremented primary key is warranted. -These are declared by adding the word `auto_increment` after the data type in the -declaration. -The datatype must be an integer. -Then the database will assign incrementing numbers at each insert. - -The example definition below defines an auto-incremented primary key - -```python -# log entries -entry_id : smallint auto_increment ---- -entry_text : varchar(4000) -entry_time = CURRENT_TIMESTAMP : timestamp(3) # automatic timestamp with millisecond precision -``` - -DataJoint passes `auto_increment` behavior to the underlying MySQL and therefore it has -the same limitation: it can only be used for tables with a single attribute in the -primary key. - -If you need to auto-increment an attribute in a composite primary key, you will need to -do so programmatically within a transaction to avoid collisions. - -For example, let’s say that you want to auto-increment `scan_idx` in a table called -`Scan` whose primary key is `(animal_id, session, scan_idx)`. -You must already have the values for `animal_id` and `session` in the dictionary `key`. -Then you can do the following: - -```python -U().aggr(Scan & key, next='max(scan_idx)+1') - -# or - -Session.aggr(Scan, next='max(scan_idx)+1') & key -``` - -Note that the first option uses a [universal set](../../query/universals.md). diff --git a/docs/src/archive/design/tables/storage-types-spec.md b/docs/src/archive/design/tables/storage-types-spec.md deleted file mode 100644 index 7157d4d42..000000000 --- a/docs/src/archive/design/tables/storage-types-spec.md +++ /dev/null @@ -1,892 +0,0 @@ -# Storage Types Redesign Spec - -## Overview - -This document defines a three-layer type architecture: - -1. **Native database types** - Backend-specific (`FLOAT`, `TINYINT UNSIGNED`, `LONGBLOB`). Discouraged for direct use. -2. **Core DataJoint types** - Standardized across backends, scientist-friendly (`float32`, `uint8`, `bool`, `json`). -3. **Codec Types** - Programmatic types with `encode()`/`decode()` semantics. Composable. - -``` -┌───────────────────────────────────────────────────────────────────┐ -│ Codec Types (Layer 3) │ -│ │ -│ Built-in: │ -│ User: ... │ -├───────────────────────────────────────────────────────────────────┤ -│ Core DataJoint Types (Layer 2) │ -│ │ -│ float32 float64 int64 uint64 int32 uint32 int16 uint16 │ -│ int8 uint8 bool uuid json bytes date datetime text │ -│ char(n) varchar(n) enum(...) decimal(n,f) │ -├───────────────────────────────────────────────────────────────────┤ -│ Native Database Types (Layer 1) │ -│ │ -│ MySQL: TINYINT SMALLINT INT BIGINT FLOAT DOUBLE ... │ -│ PostgreSQL: SMALLINT INTEGER BIGINT REAL DOUBLE PRECISION │ -│ (pass through with warning for non-standard types) │ -└───────────────────────────────────────────────────────────────────┘ -``` - -**Syntax distinction:** -- Core types: `int32`, `float64`, `varchar(255)` - no brackets -- Codec types: ``, ``, `` - angle brackets -- The `@` character indicates external storage (object store vs database) - -### OAS Storage Regions - -| Region | Path Pattern | Addressing | Use Case | -|--------|--------------|------------|----------| -| Object | `{schema}/{table}/{pk}/` | Primary key | Large objects, Zarr, HDF5 | -| Hash | `_hash/{hash}` | MD5 hash | Deduplicated blobs/files | - -### External References - -`` provides portable relative paths within configured stores with lazy ObjectRef access. -For arbitrary URLs that don't need ObjectRef semantics, use `varchar` instead. - -## Core DataJoint Types (Layer 2) - -Core types provide a standardized, scientist-friendly interface that works identically across -MySQL and PostgreSQL backends. Users should prefer these over native database types. - -**All core types are recorded in field comments using `:type:` syntax for reconstruction.** - -### Numeric Types - -| Core Type | Description | MySQL | PostgreSQL | -|-----------|-------------|-------|------------| -| `int8` | 8-bit signed | `TINYINT` | `SMALLINT` | -| `int16` | 16-bit signed | `SMALLINT` | `SMALLINT` | -| `int32` | 32-bit signed | `INT` | `INTEGER` | -| `int64` | 64-bit signed | `BIGINT` | `BIGINT` | -| `uint8` | 8-bit unsigned | `TINYINT UNSIGNED` | `SMALLINT` | -| `uint16` | 16-bit unsigned | `SMALLINT UNSIGNED` | `INTEGER` | -| `uint32` | 32-bit unsigned | `INT UNSIGNED` | `BIGINT` | -| `uint64` | 64-bit unsigned | `BIGINT UNSIGNED` | `NUMERIC(20)` | -| `float32` | 32-bit float | `FLOAT` | `REAL` | -| `float64` | 64-bit float | `DOUBLE` | `DOUBLE PRECISION` | -| `decimal(n,f)` | Fixed-point | `DECIMAL(n,f)` | `NUMERIC(n,f)` | - -### String Types - -| Core Type | Description | MySQL | PostgreSQL | -|-----------|-------------|-------|------------| -| `char(n)` | Fixed-length | `CHAR(n)` | `CHAR(n)` | -| `varchar(n)` | Variable-length | `VARCHAR(n)` | `VARCHAR(n)` | - -> **Note:** Native SQL `text` types (`text`, `tinytext`, `mediumtext`, `longtext`) are supported -> but not portable. Prefer `varchar(n)`, `json`, or `` for portable schemas. - -**Encoding:** All strings use UTF-8 (`utf8mb4` in MySQL, `UTF8` in PostgreSQL). -See [Encoding and Collation Policy](#encoding-and-collation-policy) for details. - -### Boolean - -| Core Type | Description | MySQL | PostgreSQL | -|-----------|-------------|-------|------------| -| `bool` | True/False | `TINYINT` | `BOOLEAN` | - -### Date/Time Types - -| Core Type | Description | MySQL | PostgreSQL | -|-----------|-------------|-------|------------| -| `date` | Date only | `DATE` | `DATE` | -| `datetime` | Date and time | `DATETIME` | `TIMESTAMP` | - -**Timezone policy:** All `datetime` values should be stored as **UTC**. Timezone conversion is a -presentation concern handled by the application layer, not the database. This ensures: -- Reproducible computations regardless of server or client timezone settings -- Simple arithmetic on temporal values (no DST ambiguity) -- Portable data across systems and regions - -Use `CURRENT_TIMESTAMP` for auto-populated creation times: -``` -created_at : datetime = CURRENT_TIMESTAMP -``` - -### Binary Types - -The core `bytes` type stores raw bytes without any serialization. Use the `` codec -for serialized Python objects. - -| Core Type | Description | MySQL | PostgreSQL | -|-----------|-------------|-------|------------| -| `bytes` | Raw bytes | `LONGBLOB` | `BYTEA` | - -### Other Types - -| Core Type | Description | MySQL | PostgreSQL | -|-----------|-------------|-------|------------| -| `json` | JSON document | `JSON` | `JSONB` | -| `uuid` | UUID | `BINARY(16)` | `UUID` | -| `enum(...)` | Enumeration | `ENUM(...)` | `CREATE TYPE ... AS ENUM` | - -### Native Passthrough Types - -Users may use native database types directly (e.g., `mediumint`, `tinyblob`), -but these will generate a warning about non-standard usage. Native types are not recorded -in field comments and may have portability issues across database backends. - -### Type Modifiers Policy - -DataJoint table definitions have their own syntax for constraints and metadata. SQL type -modifiers are **not allowed** in type specifications because they conflict with DataJoint's -declarative syntax: - -| Modifier | Status | DataJoint Alternative | -|----------|--------|----------------------| -| `NOT NULL` / `NULL` | ❌ Not allowed | Use `= NULL` for nullable; omit default for required | -| `DEFAULT value` | ❌ Not allowed | Use `= value` syntax before the type | -| `PRIMARY KEY` | ❌ Not allowed | Position above `---` line | -| `UNIQUE` | ❌ Not allowed | Use DataJoint index syntax | -| `COMMENT 'text'` | ❌ Not allowed | Use `# comment` syntax | -| `CHARACTER SET` | ❌ Not allowed | Database-level configuration | -| `COLLATE` | ❌ Not allowed | Database-level configuration | -| `AUTO_INCREMENT` | ⚠️ Discouraged | Allowed with native types only, generates warning | -| `UNSIGNED` | ✅ Allowed | Part of type semantics (use `uint*` core types) | - -**Nullability and defaults:** DataJoint handles nullability through the default value syntax. -An attribute is nullable if and only if its default is `NULL`: - -``` -# Required (NOT NULL, no default) -name : varchar(100) - -# Nullable (default is NULL) -nickname = NULL : varchar(100) - -# Required with default value -status = "active" : varchar(20) -``` - -**Auto-increment policy:** DataJoint discourages `AUTO_INCREMENT` / `SERIAL` because: -- Breaks reproducibility (IDs depend on insertion order) -- Makes pipelines non-deterministic -- Complicates data migration and replication -- Primary keys should be meaningful, not arbitrary - -If required, use native types: `int auto_increment` or `serial` (with warning). - -### Encoding and Collation Policy - -Character encoding and collation are **database-level configuration**, not part of type -definitions. This ensures consistent behavior across all tables and simplifies portability. - -**Configuration** (in `dj.config` or `datajoint.json`): -```json -{ - "database.charset": "utf8mb4", - "database.collation": "utf8mb4_bin" -} -``` - -**Defaults:** - -| Setting | MySQL | PostgreSQL | -|---------|-------|------------| -| Charset | `utf8mb4` | `UTF8` | -| Collation | `utf8mb4_bin` | `C` | - -**Policy:** -- **UTF-8 required**: DataJoint validates charset is UTF-8 compatible at connection time -- **Case-sensitive by default**: Binary collation (`utf8mb4_bin` / `C`) ensures predictable comparisons -- **No per-column overrides**: `CHARACTER SET` and `COLLATE` are rejected in type definitions -- **Like timezone**: Encoding is infrastructure configuration, not part of the data model - -## Codec Types (Layer 3) - -Codec types provide `encode()`/`decode()` semantics on top of core types. They are -composable and can be built-in or user-defined. - -### Storage Mode: `@` Convention - -The `@` character in codec syntax indicates **external storage** (object store): - -- **No `@`**: Internal storage (database) - e.g., ``, `` -- **`@` present**: External storage (object store) - e.g., ``, `` -- **`@` alone**: Use default store - e.g., `` -- **`@name`**: Use named store - e.g., `` - -Some codecs support both modes (``, ``), others are external-only (``, ``, ``). - -### Codec Base Class - -Codecs auto-register when subclassed using Python's `__init_subclass__` mechanism. -No decorator is needed. - -```python -from abc import ABC, abstractmethod -from typing import Any - -# Global codec registry -_codec_registry: dict[str, "Codec"] = {} - - -class Codec(ABC): - """ - Base class for codec types. Subclasses auto-register by name. - - Requires Python 3.10+. - """ - name: str | None = None # Must be set by concrete subclasses - - def __init_subclass__(cls, *, register: bool = True, **kwargs): - """Auto-register concrete codecs when subclassed.""" - super().__init_subclass__(**kwargs) - - if not register: - return # Skip registration for abstract bases - - if cls.name is None: - return # Skip registration if no name (abstract) - - if cls.name in _codec_registry: - existing = _codec_registry[cls.name] - if type(existing) is not cls: - raise DataJointError( - f"Codec <{cls.name}> already registered by {type(existing).__name__}" - ) - return # Same class, idempotent - - _codec_registry[cls.name] = cls() - - def get_dtype(self, is_external: bool) -> str: - """ - Return the storage dtype for this codec. - - Args: - is_external: True if @ modifier present (external storage) - - Returns: - A core type (e.g., "bytes", "json") or another codec (e.g., "") - """ - raise NotImplementedError - - @abstractmethod - def encode(self, value: Any, *, key: dict | None = None, store_name: str | None = None) -> Any: - """Encode Python value for storage.""" - ... - - @abstractmethod - def decode(self, stored: Any, *, key: dict | None = None) -> Any: - """Decode stored value back to Python.""" - ... - - def validate(self, value: Any) -> None: - """Optional validation before encoding. Override to add constraints.""" - pass - - -def list_codecs() -> list[str]: - """Return list of registered codec names.""" - return sorted(_codec_registry.keys()) - - -def get_codec(name: str) -> Codec: - """Get codec by name. Raises DataJointError if not found.""" - if name not in _codec_registry: - raise DataJointError(f"Unknown codec: <{name}>") - return _codec_registry[name] -``` - -**Usage - no decorator needed:** - -```python -class GraphCodec(dj.Codec): - """Auto-registered as .""" - name = "graph" - - def get_dtype(self, is_external: bool) -> str: - return "" - - def encode(self, graph, *, key=None, store_name=None): - return {'nodes': list(graph.nodes()), 'edges': list(graph.edges())} - - def decode(self, stored, *, key=None): - import networkx as nx - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - G.add_edges_from(stored['edges']) - return G -``` - -**Skip registration for abstract bases:** - -```python -class ExternalOnlyCodec(dj.Codec, register=False): - """Abstract base for external-only codecs. Not registered.""" - - def get_dtype(self, is_external: bool) -> str: - if not is_external: - raise DataJointError(f"<{self.name}> requires @ (external only)") - return "json" -``` - -### Codec Resolution and Chaining - -Codecs resolve to core types through chaining. The `get_dtype(is_external)` method -returns the appropriate dtype based on storage mode: - -``` -Resolution at declaration time: - - → get_dtype(False) → "bytes" → LONGBLOB/BYTEA - → get_dtype(True) → "" → json → JSON/JSONB - → get_dtype(True) → "" → json (store=cold) - - → get_dtype(False) → "bytes" → LONGBLOB/BYTEA - → get_dtype(True) → "" → json → JSON/JSONB - - → get_dtype(True) → "json" → JSON/JSONB - → get_dtype(False) → ERROR (external only) - - → get_dtype(True) → "json" → JSON/JSONB - → get_dtype(True) → "json" → JSON/JSONB -``` - -### `` / `` - Path-Addressed Storage - -**Built-in codec. External only.** - -OAS (Object-Augmented Schema) storage for files and folders: - -- Path derived from primary key: `{schema}/{table}/{pk}/{attribute}/` -- One-to-one relationship with table row -- Deleted when row is deleted -- Returns `ObjectRef` for lazy access -- Supports direct writes (Zarr, HDF5) via fsspec -- **dtype**: `json` (stores path, store name, metadata) - -```python -class Analysis(dj.Computed): - definition = """ - -> Recording - --- - results : # default store - archive : # specific store - """ -``` - -#### Implementation - -```python -class ObjectCodec(dj.Codec): - """Path-addressed OAS storage. External only.""" - name = "object" - - def get_dtype(self, is_external: bool) -> str: - if not is_external: - raise DataJointError(" requires @ (external storage only)") - return "json" - - def encode(self, value, *, key=None, store_name=None) -> dict: - store = get_store(store_name or dj.config['stores']['default']) - path = self._compute_path(key) # {schema}/{table}/{pk}/{attr}/ - store.put(path, value) - return {"path": path, "store": store_name, ...} - - def decode(self, stored: dict, *, key=None) -> ObjectRef: - return ObjectRef(store=get_store(stored["store"]), path=stored["path"]) -``` - -### `` / `` - Hash-Addressed Storage - -**Built-in codec. External only.** - -Hash-addressed storage with deduplication: - -- **Single blob only**: stores a single file or serialized object (not folders) -- **Per-project scope**: content is shared across all schemas in a project (not per-schema) -- Path derived from content hash: `_hash/{hash[:2]}/{hash[2:4]}/{hash}` -- Many-to-one: multiple rows (even across schemas) can reference same content -- Reference counted for garbage collection -- Deduplication: identical content stored once across the entire project -- For folders/complex objects, use `object` type instead -- **dtype**: `json` (stores hash, store name, size, metadata) - -``` -store_root/ -├── {schema}/{table}/{pk}/ # object storage (path-addressed by PK) -│ └── {attribute}/ -│ -└── _hash/ # content storage (hash-addressed) - └── {hash[:2]}/{hash[2:4]}/{hash} -``` - -#### Implementation - -```python -class HashCodec(dj.Codec): - """Hash-addressed storage. External only.""" - name = "hash" - - def get_dtype(self, is_external: bool) -> str: - if not is_external: - raise DataJointError(" requires @ (external storage only)") - return "json" - - def encode(self, data: bytes, *, key=None, store_name=None) -> dict: - """Store content, return metadata as JSON.""" - hash_id = hashlib.md5(data).hexdigest() # 32-char hex - store = get_store(store_name or dj.config['stores']['default']) - path = f"_hash/{hash_id[:2]}/{hash_id[2:4]}/{hash_id}" - - if not store.exists(path): - store.put(path, data) - - # Metadata stored in JSON column (no separate registry) - return {"hash": hash_id, "store": store_name, "size": len(data)} - - def decode(self, stored: dict, *, key=None) -> bytes: - """Retrieve content by hash.""" - store = get_store(stored["store"]) - path = f"_hash/{stored['hash'][:2]}/{stored['hash'][2:4]}/{stored['hash']}" - return store.get(path) -``` - -#### Database Column - -The `` type stores JSON metadata: - -```sql --- content column (MySQL) -features JSON NOT NULL --- Contains: {"hash": "abc123...", "store": "main", "size": 12345} - --- content column (PostgreSQL) -features JSONB NOT NULL -``` - -### `` - Portable External Reference - -**Built-in codec. External only (store required).** - -Relative path references within configured stores: - -- **Relative paths**: paths within a configured store (portable across environments) -- **Store-aware**: resolves paths against configured store backend -- Returns `ObjectRef` for lazy access via fsspec -- Stores optional checksum for verification -- **dtype**: `json` (stores path, store name, checksum, metadata) - -**Key benefit**: Portability. The path is relative to the store, so pipelines can be moved -between environments (dev → prod, cloud → local) by changing store configuration without -updating data. - -```python -class RawData(dj.Manual): - definition = """ - session_id : int32 - --- - recording : # relative path within 'main' store - """ - -# Insert - user provides relative path within the store -table.insert1({ - 'session_id': 1, - 'recording': 'experiment_001/data.nwb' # relative to main store root -}) - -# Fetch - returns ObjectRef (lazy) -row = (table & 'session_id=1').fetch1() -ref = row['recording'] # ObjectRef -ref.download('/local/path') # explicit download -ref.open() # fsspec streaming access -``` - -#### When to Use `` vs `varchar` - -| Use Case | Recommended Type | -|----------|------------------| -| Need ObjectRef/lazy access | `` | -| Need portability (relative paths) | `` | -| Want checksum verification | `` | -| Just storing a URL string | `varchar` | -| External URLs you don't control | `varchar` | - -For arbitrary URLs (S3, HTTP, etc.) where you don't need ObjectRef semantics, -just use `varchar`. A string is simpler and more transparent. - -#### Implementation - -```python -class FilepathCodec(dj.Codec): - """Store-relative file references. External only.""" - name = "filepath" - - def get_dtype(self, is_external: bool) -> str: - if not is_external: - raise DataJointError(" requires @store") - return "json" - - def encode(self, relative_path: str, *, key=None, store_name=None) -> dict: - """Register reference to file in store.""" - store = get_store(store_name) # store_name required for filepath - return {'path': relative_path, 'store': store_name} - - def decode(self, stored: dict, *, key=None) -> ObjectRef: - """Return ObjectRef for lazy access.""" - return ObjectRef(store=get_store(stored['store']), path=stored['path']) -``` - -#### Database Column - -```sql --- filepath column (MySQL) -recording JSON NOT NULL --- Contains: {"path": "experiment_001/data.nwb", "store": "main", "checksum": "...", "size": ...} - --- filepath column (PostgreSQL) -recording JSONB NOT NULL -``` - -#### Key Differences from Legacy `filepath@store` (now ``) - -| Feature | Legacy | New | -|---------|--------|-----| -| Access | Copy to local stage | ObjectRef (lazy) | -| Copying | Automatic | Explicit via `ref.download()` | -| Streaming | No | Yes via `ref.open()` | -| Paths | Relative | Relative (unchanged) | -| Store param | Required (`@store`) | Required (`@store`) | - -## Database Types - -### `json` - Cross-Database JSON Type - -JSON storage compatible across MySQL and PostgreSQL: - -```sql --- MySQL -column_name JSON NOT NULL - --- PostgreSQL (uses JSONB for better indexing) -column_name JSONB NOT NULL -``` - -The `json` database type: -- Used as dtype by built-in codecs (``, ``, ``) -- Stores arbitrary JSON-serializable data -- Automatically uses appropriate type for database backend -- Supports JSON path queries where available - -## Built-in Codecs - -### `` / `` - Serialized Python Objects - -**Supports both internal and external storage.** - -Serializes Python objects (NumPy arrays, dicts, lists, etc.) using DataJoint's -blob format. Compatible with MATLAB. - -- **``**: Stored in database (`bytes` → `LONGBLOB`/`BYTEA`) -- **``**: Stored externally via `` with deduplication -- **``**: Stored in specific named store - -```python -class BlobCodec(dj.Codec): - """Serialized Python objects. Supports internal and external.""" - name = "blob" - - def get_dtype(self, is_external: bool) -> str: - return "" if is_external else "bytes" - - def encode(self, value, *, key=None, store_name=None) -> bytes: - from . import blob - return blob.pack(value, compress=True) - - def decode(self, stored, *, key=None) -> Any: - from . import blob - return blob.unpack(stored) -``` - -Usage: -```python -class ProcessedData(dj.Computed): - definition = """ - -> RawData - --- - small_result : # internal (in database) - large_result : # external (default store) - archive_result : # external (specific store) - """ -``` - -### `` / `` - File Attachments - -**Supports both internal and external storage.** - -Stores files with filename preserved. On fetch, extracts to configured download path. - -- **``**: Stored in database (`bytes` → `LONGBLOB`/`BYTEA`) -- **``**: Stored externally via `` with deduplication -- **``**: Stored in specific named store - -```python -class AttachCodec(dj.Codec): - """File attachment with filename. Supports internal and external.""" - name = "attach" - - def get_dtype(self, is_external: bool) -> str: - return "" if is_external else "bytes" - - def encode(self, filepath, *, key=None, store_name=None) -> bytes: - path = Path(filepath) - return path.name.encode() + b"\0" + path.read_bytes() - - def decode(self, stored, *, key=None) -> str: - filename, contents = stored.split(b"\0", 1) - filename = filename.decode() - download_path = Path(dj.config['download_path']) / filename - download_path.write_bytes(contents) - return str(download_path) -``` - -Usage: -```python -class Attachments(dj.Manual): - definition = """ - attachment_id : int32 - --- - config : # internal (small file in DB) - data_file : # external (default store) - archive : # external (specific store) - """ -``` - -## User-Defined Codecs - -Users can define custom codecs for domain-specific data: - -```python -class GraphCodec(dj.Codec): - """Store NetworkX graphs. Internal only (no external support).""" - name = "graph" - - def get_dtype(self, is_external: bool) -> str: - if is_external: - raise DataJointError(" does not support external storage") - return "" # Chain to blob for serialization - - def encode(self, graph, *, key=None, store_name=None): - return {'nodes': list(graph.nodes()), 'edges': list(graph.edges())} - - def decode(self, stored, *, key=None): - import networkx as nx - G = nx.Graph() - G.add_nodes_from(stored['nodes']) - G.add_edges_from(stored['edges']) - return G -``` - -Custom codecs can support both modes by returning different dtypes: - -```python -class ImageCodec(dj.Codec): - """Store images. Supports both internal and external.""" - name = "image" - - def get_dtype(self, is_external: bool) -> str: - return "" if is_external else "bytes" - - def encode(self, image, *, key=None, store_name=None) -> bytes: - # Convert PIL Image to PNG bytes - buffer = io.BytesIO() - image.save(buffer, format='PNG') - return buffer.getvalue() - - def decode(self, stored: bytes, *, key=None): - return PIL.Image.open(io.BytesIO(stored)) -``` - -## Storage Comparison - -| Type | get_dtype | Resolves To | Storage Location | Dedup | Returns | -|------|-----------|-------------|------------------|-------|---------| -| `` | `bytes` | `LONGBLOB`/`BYTEA` | Database | No | Python object | -| `` | `` | `json` | `_hash/{hash}` | Yes | Python object | -| `` | `` | `json` | `_hash/{hash}` | Yes | Python object | -| `` | `bytes` | `LONGBLOB`/`BYTEA` | Database | No | Local file path | -| `` | `` | `json` | `_hash/{hash}` | Yes | Local file path | -| `` | `` | `json` | `_hash/{hash}` | Yes | Local file path | -| `` | `json` | `JSON`/`JSONB` | `{schema}/{table}/{pk}/` | No | ObjectRef | -| `` | `json` | `JSON`/`JSONB` | `{schema}/{table}/{pk}/` | No | ObjectRef | -| `` | `json` | `JSON`/`JSONB` | `_hash/{hash}` | Yes | bytes | -| `` | `json` | `JSON`/`JSONB` | `_hash/{hash}` | Yes | bytes | -| `` | `json` | `JSON`/`JSONB` | Configured store | No | ObjectRef | - -## Garbage Collection for Hash Storage - -Hash metadata (hash, store, size) is stored directly in each table's JSON column - no separate -registry table is needed. Garbage collection scans all tables to find referenced hashes: - -```python -def garbage_collect(store_name): - """Remove hash-addressed data not referenced by any table.""" - # Scan store for all hash files - store = get_store(store_name) - all_hashes = set(store.list_hashes()) # from _hash/ directory - - # Scan all tables for referenced hashes - referenced = set() - for schema in project.schemas: - for table in schema.tables: - for attr in table.heading.attributes: - if uses_hash_storage(attr): # , , - for row in table.fetch(attr.name): - if row and row.get('store') == store_name: - referenced.add(row['hash']) - - # Delete orphaned files - for hash_id in (all_hashes - referenced): - store.delete(hash_path(hash_id)) -``` - -## Built-in Codec Comparison - -| Feature | `` | `` | `` | `` | `` | -|---------|----------|------------|-------------|--------------|---------------| -| Storage modes | Both | Both | External only | External only | External only | -| Internal dtype | `bytes` | `bytes` | N/A | N/A | N/A | -| External dtype | `` | `` | `json` | `json` | `json` | -| Addressing | Hash | Hash | Primary key | Hash | Relative path | -| Deduplication | Yes (external) | Yes (external) | No | Yes | No | -| Structure | Single blob | Single file | Files, folders | Single blob | Any | -| Returns | Python object | Local path | ObjectRef | bytes | ObjectRef | -| GC | Ref counted | Ref counted | With row | Ref counted | User managed | - -**When to use each:** -- **``**: Serialized Python objects (NumPy arrays, dicts). Use `` for large/duplicated data -- **``**: File attachments with filename preserved. Use `` for large files -- **``**: Large/complex file structures (Zarr, HDF5) where DataJoint controls organization -- **``**: Raw bytes with deduplication (typically used via `` or ``) -- **``**: Portable references to externally-managed files -- **`varchar`**: Arbitrary URLs/paths where ObjectRef semantics aren't needed - -## Key Design Decisions - -1. **Three-layer architecture**: - - Layer 1: Native database types (backend-specific, discouraged) - - Layer 2: Core DataJoint types (standardized, scientist-friendly) - - Layer 3: Codec types (encode/decode, composable) -2. **Core types are scientist-friendly**: `float32`, `uint8`, `bool`, `bytes` instead of `FLOAT`, `TINYINT UNSIGNED`, `LONGBLOB` -3. **Codecs use angle brackets**: ``, ``, `` - distinguishes from core types -4. **`@` indicates external storage**: No `@` = database, `@` present = object store -5. **`get_dtype(is_external)` method**: Codecs resolve dtype at declaration time based on storage mode -6. **Codecs are composable**: `` uses ``, which uses `json` -7. **Built-in external codecs use JSON dtype**: Stores metadata (path, hash, store name, etc.) -8. **Two OAS regions**: object (PK-addressed) and hash (hash-addressed) within managed stores -9. **Filepath for portability**: `` uses relative paths within stores for environment portability -10. **No `uri` type**: For arbitrary URLs, use `varchar`—simpler and more transparent -11. **Naming conventions**: - - `@` = external storage (object store) - - No `@` = internal storage (database) - - `@` alone = default store - - `@name` = named store -12. **Dual-mode codecs**: `` and `` support both internal and external storage -13. **External-only codecs**: ``, ``, `` require `@` -14. **Transparent access**: Codecs return Python objects or file paths -15. **Lazy access**: `` and `` return ObjectRef -16. **MD5 for content hashing**: See [Hash Algorithm Choice](#hash-algorithm-choice) below -17. **No separate registry**: Hash metadata stored in JSON columns, not a separate table -18. **Auto-registration via `__init_subclass__`**: Codecs register automatically when subclassed—no decorator needed. Use `register=False` for abstract bases. Requires Python 3.10+. - -### Hash Algorithm Choice - -Content-addressed storage uses **MD5** (128-bit, 32-char hex) rather than SHA256 (256-bit, 64-char hex). - -**Rationale:** - -1. **Practical collision resistance is sufficient**: The birthday bound for MD5 is ~2^64 operations - before 50% collision probability. No scientific project will store anywhere near 10^19 files. - For content deduplication (not cryptographic verification), MD5 provides adequate uniqueness. - -2. **Storage efficiency**: 32-char hashes vs 64-char hashes in every JSON metadata field. - With millions of records, this halves the storage overhead for hash identifiers. - -3. **Performance**: MD5 is ~2-3x faster than SHA256 for large files. While both are fast, - the difference is measurable when hashing large scientific datasets. - -4. **Legacy compatibility**: DataJoint's existing `uuid_from_buffer()` function uses MD5. - The new system changes only the storage format (hex string in JSON vs binary UUID), - not the underlying hash algorithm. This simplifies migration. - -5. **Consistency with existing codebase**: The `dj.hash` module already uses MD5 for - `key_hash()` (job reservation) and `uuid_from_buffer()` (query caching). - -**Why not SHA256?** - -SHA256 is the modern standard for content-addressable storage (Git, Docker, IPFS). However: -- These systems prioritize cryptographic security against adversarial collision attacks -- Scientific data pipelines face no adversarial threat model -- The practical benefits (storage, speed, compatibility) outweigh theoretical security gains - -**Note**: If cryptographic verification is ever needed (e.g., for compliance or reproducibility -audits), SHA256 checksums can be computed on-demand without changing the storage addressing scheme. - -## Migration from Legacy Types - -| Legacy | New Equivalent | -|--------|----------------| -| `longblob` (auto-serialized) | `` | -| `blob@store` | `` | -| `attach` | `` | -| `attach@store` | `` | -| `filepath@store` (copy-based) | `` (ObjectRef-based) | - -### Migration from Legacy `~external_*` Stores - -Legacy external storage used per-schema `~external_{store}` tables with UUID references. -Migration to the new JSON-based hash storage requires: - -```python -def migrate_external_store(schema, store_name): - """ - Migrate legacy ~external_{store} to new HashRegistry. - - 1. Read all entries from ~external_{store} - 2. For each entry: - - Fetch content from legacy location - - Compute MD5 hash - - Copy to _hash/{hash}/ if not exists - - Update table column to new hash format - 3. After all schemas migrated, drop ~external_{store} tables - """ - external_table = schema.external[store_name] - - for entry in external_table.fetch(as_dict=True): - legacy_uuid = entry['hash'] - - # Fetch content from legacy location - content = external_table.get(legacy_uuid) - - # Compute new content hash - hash_id = hashlib.md5(content).hexdigest() - - # Store in new location if not exists - new_path = f"_hash/{hash_id[:2]}/{hash_id[2:4]}/{hash_id}" - store = get_store(store_name) - if not store.exists(new_path): - store.put(new_path, content) - - # Update referencing tables: convert UUID column to JSON with hash metadata - # The JSON column stores {"hash": hash_id, "store": store_name, "size": len(content)} - # ... update all tables that reference this UUID ... - - # After migration complete for all schemas: - # DROP TABLE `{schema}`.`~external_{store}` -``` - -**Migration considerations:** -- Legacy UUIDs were based on MD5 content hash stored as `binary(16)` (UUID format) -- New system uses `char(32)` MD5 hex strings stored in JSON -- The hash algorithm is unchanged (MD5), only the storage format differs -- Migration can be done incrementally per schema -- Backward compatibility layer can read both formats during transition - -## Open Questions - -1. How long should the backward compatibility layer support legacy `~external_*` format? -2. Should `` (without store name) use a default store or require explicit store name? diff --git a/docs/src/archive/design/tables/tiers.md b/docs/src/archive/design/tables/tiers.md deleted file mode 100644 index 2cf1f9428..000000000 --- a/docs/src/archive/design/tables/tiers.md +++ /dev/null @@ -1,68 +0,0 @@ -# Data Tiers - -DataJoint assigns all tables to one of the following data tiers that differentiate how -the data originate. - -## Table tiers - -| Tier | Superclass | Description | -| -- | -- | -- | -| Lookup | `dj.Lookup` | Small tables containing general facts and settings of the data pipeline; not specific to any experiment or dataset. | -| Manual | `dj.Manual` | Data entered from outside the pipeline, either by hand or with external helper scripts. | -| Imported | `dj.Imported` | Data ingested automatically inside the pipeline but requiring access to data outside the pipeline. | -| Computed | `dj.Computed` | Data computed automatically entirely inside the pipeline. | - -Table data tiers indicate to database administrators how valuable the data are. -Manual data are the most valuable, as re-entry may be tedious or impossible. -Computed data are safe to delete, as the data can always be recomputed from within DataJoint. -Imported data are safer than manual data but less safe than computed data because of -dependency on external data sources. -With these considerations, database administrators may opt not to back up computed -data, for example, or to back up imported data less frequently than manual data. - -The data tier of a table is specified by the superclass of its class. -For example, the User class in [definitions](declare.md) uses the `dj.Manual` -superclass. -Therefore, the corresponding User table on the database would be of the Manual tier. -Furthermore, the classes for **imported** and **computed** tables have additional -capabilities for automated processing as described in -[Auto-populate](../../compute/populate.md). - -## Internal conventions for naming tables - -On the server side, DataJoint uses a naming scheme to generate a table name -corresponding to a given class. -The naming scheme includes prefixes specifying each table's data tier. - -First, the name of the class is converted from `CamelCase` to `snake_case` -([separation by underscores](https://en.wikipedia.org/wiki/Snake_case)). -Then the name is prefixed according to the data tier. - -- `Manual` tables have no prefix. -- `Lookup` tables are prefixed with `#`. -- `Imported` tables are prefixed with `_`, a single underscore. -- `Computed` tables are prefixed with `__`, two underscores. - -For example: - -The table for the class `StructuralScan` subclassing `dj.Manual` will be named -`structural_scan`. - -The table for the class `SpatialFilter` subclassing `dj.Lookup` will be named -`#spatial_filter`. - -Again, the internal table names including prefixes are used only on the server side. -These are never visible to the user, and DataJoint users do not need to know these -conventions -However, database administrators may use these naming patterns to set backup policies -or to restrict access based on data tiers. - -## Part tables - -[Part tables](master-part.md) do not have their own tier. -Instead, they share the same tier as their master table. -The prefix for part tables also differs from the other tiers. -They are prefixed by the name of their master table, separated by two underscores. - -For example, the table for the class `Channel(dj.Part)` with the master -`Ephys(dj.Imported)` will be named `_ephys__channel`. diff --git a/docs/src/archive/faq.md b/docs/src/archive/faq.md deleted file mode 100644 index c4c82d014..000000000 --- a/docs/src/archive/faq.md +++ /dev/null @@ -1,192 +0,0 @@ -# Frequently Asked Questions - -## How do I use DataJoint with a GUI? - -It is common to enter data during experiments using a graphical user interface. - -1. The [DataJoint platform](https://works.datajoint.com) platform is a web-based, - end-to-end platform to host and execute data pipelines. - -2. [DataJoint LabBook](https://github.com/datajoint/datajoint-labbook) is an open -source project for data entry but is no longer actively maintained. - -## Does DataJoint support other programming languages? - -DataJoint [Python](https://docs.datajoint.com/core/datajoint-python/) is the most -up-to-date version and all future development will focus on the Python API. The -[Matlab](https://datajoint.com/docs/core/datajoint-matlab/) API was actively developed -through 2023. Previous projects implemented some DataJoint features in -[Julia](https://github.com/BrainCOGS/neuronex_workshop_2018/tree/julia/julia) and -[Rust](https://github.com/datajoint/datajoint-core). DataJoint's data model and data -representation are largely language independent, which means that any language with a -DataJoint client can work with a data pipeline defined in any other language. DataJoint -clients for other programming languages will be implemented based on demand. All -languages must comply to the same data model and computation approach as defined in -[DataJoint: a simpler relational data model](https://arxiv.org/abs/1807.11104). - -## Can I use DataJoint with my current database? - -Researchers use many different tools to keep records, from simple formalized file -hierarchies to complete software packages for colony management and standard file types -like NWB. Existing projects have built interfaces with many such tools, such as -[PyRAT](https://github.com/SFB1089/adamacs/blob/main/notebooks/03_pyrat_insert.ipynb). -The only requirement for interface is that tool has an open API. Contact -[support@datajoint.com](mailto:Support@DataJoint.com) with inquiries. The DataJoint -team will consider development requests based on community demand. - -## Is DataJoint an ORM? - -Programmers are familiar with object-relational mappings (ORM) in various programming -languages. Python in particular has several popular ORMs such as -[SQLAlchemy](https://www.sqlalchemy.org/) and [Django ORM](https://tutorial.djangogirls.org/en/django_orm/). -The purpose of ORMs is to allow representations and manipulations of objects from the -host programming language as data in a relational database. ORMs allow making objects -persistent between program executions by creating a bridge (i.e., mapping) between the -object model used by the host language and the relational model allowed by the database. -The result is always a compromise, usually toward the object model. ORMs usually forgo -key concepts, features, and capabilities of the relational model for the sake of -convenient programming constructs in the language. - -In contrast, DataJoint implements a data model that is a refinement of the relational -data model without compromising its core principles of data representation and queries. -DataJoint supports data integrity (entity integrity, referential integrity, and group -integrity) and provides a fully capable relational query language. DataJoint remains -absolutely data-centric, with the primary focus on the structure and integrity of the -data pipeline. Other ORMs are more application-centric, primarily focusing on the -application design while the database plays a secondary role supporting the application -with object persistence and sharing. - -## What is the difference between DataJoint and Alyx? - -[Alyx](https://github.com/cortex-lab/alyx) is an experiment management database -application developed in Kenneth Harris' lab at UCL. - -Alyx is an application with a fixed pipeline design with a nice graphical user -interface. In contrast, DataJoint is a general-purpose library for designing and -building data processing pipelines. - -Alyx is geared towards ease of data entry and tracking for a specific workflow -(e.g. mouse colony information and some pre-specified experiments) and data types. -DataJoint could be used as a more general purposes tool to design, implement, and -execute processing on such workflows/pipelines from scratch, and DataJoint focuses on -flexibility, data integrity, and ease of data analysis. The purposes are partly -overlapping and complementary. The -[International Brain Lab project](https://internationalbrainlab.com) is developing a -bridge from Alyx to DataJoint, hosted as an -[open-source project](https://github.com/datajoint-company/ibl-pipeline). It -implements a DataJoint schema that replicates the major features of the Alyx -application and a synchronization script from an existing Alyx database to its -DataJoint counterpart. - -## Where is my data? - -New users often ask this question thinking of passive **data repositories** -- -collections of files and folders and a separate collection of metadata -- information -about how the files were collected and what they contain. -Let's address metadata first, since the answer there is easy: Everything goes in the -database! -Any information about the experiment that would normally be stored in a lab notebook, -in an Excel spreadsheet, or in a Word document is entered into tables in the database. -These tables can accommodate numbers, strings, dates, or numerical arrays. -The entry of metadata can be manual, or it can be an automated part of data acquisition -(in this case the acquisition software itself is modified to enter information directly -into the database). - -Depending on their size and contents, raw data files can be stored in a number of ways. -In the simplest and most common scenario, raw data continues to be stored in either a -local filesystem or in the cloud as collections of files and folders. -The paths to these files are entered in the database (again, either manually or by -automated processes). -This is the point at which the notion of a **data pipeline** begins. -Below these "manual tables" that contain metadata and file paths are a series of tables -that load raw data from these files, process it in some way, and insert derived or -summarized data directly into the database. -For example, in an imaging application, the very large raw `.TIFF` stacks would reside on -the filesystem, but the extracted fluorescent trace timeseries for each cell in the -image would be stored as a numerical array directly in the database. -Or the raw video used for animal tracking might be stored in a standard video format on -the filesystem, but the computed X/Y positions of the animal would be stored in the -database. -Storing these intermediate computations in the database makes them easily available for -downstream analyses and queries. - -## Do I have to manually enter all my data into the database? - -No! While some of the data will be manually entered (the same way that it would be -manually recorded in a lab notebook), the advantage of DataJoint is that standard -downstream processing steps can be run automatically on all new data with a single -command. -This is where the notion of a **data pipeline** comes into play. -When the workflow of cleaning and processing the data, extracting important features, -and performing basic analyses is all implemented in a DataJoint pipeline, minimal -effort is required to analyze newly-collected data. -Depending on the size of the raw files and the complexity of analysis, useful results -may be available in a matter of minutes or hours. -Because these results are stored in the database, they can be made available to anyone -who is given access credentials for additional downstream analyses. - -## Won't the database get too big if all my data are there? - -Typically, this is not a problem. -If you find that your database is getting larger than a few dozen TB, DataJoint -provides transparent solutions for storing very large chunks of data (larger than the 4 -GB that can be natively stored as a LONGBLOB in MySQL). -However, in many scenarios even long time series or images can be stored directly in -the database with little effect on performance. - -## Why not just process the data and save them back to a file? - -There are two main advantages to storing results in the database. -The first is data integrity. -Because the relationships between data are enforced by the structure of the database, -DataJoint ensures that the metadata in the upstream nodes always correctly describes -the computed results downstream in the pipeline. -If a specific experimental session is deleted, for example, all the data extracted from -that session are automatically removed as well, so there is no chance of "orphaned" -data. -Likewise, the database ensures that computations are atomic. -This means that any computation performed on a dataset is performed in an all-or-none -fashion. -Either all of the data are processed and inserted, or none at all. -This ensures that there are no incomplete data. -Neither of these important features of data integrity can be guaranteed by a file -system. - -The second advantage of storing intermediate results in a data pipeline is flexible -access. -Accessing arbitrarily complex subsets of the data can be achieved with DataJoint's -flexible query language. -When data are stored in files, collecting the desired data requires trawling through -the file hierarchy, finding and loading the files of interest, and selecting the -interesting parts of the data. - -This brings us to the final important question: - -## How do I get my data out? - -This is the fun part. See [queries](query/operators.md) for details of the DataJoint -query language directly from Python. - -## Interfaces - -Multiple interfaces may be used to get the data into and out of the pipeline. - -Some labs use third-party GUI applications such as -[HeidiSQL](https://www.heidisql.com/) and -[Navicat](https://www.navicat.com/), for example. These applications allow entering -and editing data in tables similarly to spreadsheets. - -The Helium Application (https://mattbdean.github.io/Helium/ and -https://github.com/mattbdean/Helium) is web application for browsing DataJoint -pipelines and entering new data. -Matt Dean develops and maintains Helium under the direction of members of Karel -Svoboda's lab at Janelia Research Campus and Vathes LLC. - -Data may also be imported or synchronized into a DataJoint pipeline from existing LIMS -(laboratory information management systems). -For example, the [International Brain Lab](https://internationalbrainlab.com) -synchronizes data from an [Alyx database](https://github.com/cortex-lab/alyx). -For implementation details, see https://github.com/int-brain-lab/IBL-pipeline. - -Other labs (e.g. Sinz Lab) have developed GUI interfaces using the Flask web framework -in Python. diff --git a/docs/src/archive/images/StudentTable.png b/docs/src/archive/images/StudentTable.png deleted file mode 100644 index c8623f2ab7368b57519b7ef5b7ce98fec6570e07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48049 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 Jt#e9f0s!0+!LI-S diff --git a/docs/src/archive/images/added-example-ERD.svg b/docs/src/archive/images/added-example-ERD.svg deleted file mode 100644 index 0884853f4..000000000 --- a/docs/src/archive/images/added-example-ERD.svg +++ /dev/null @@ -1,207 +0,0 @@ - - - -%3 - - - -uni.Term - - -uni.Term - - - - - -uni.Section - - -uni.Section - - - - - -uni.Term->uni.Section - - - - -uni.CurrentTerm - - -uni.CurrentTerm - - - - - -uni.Term->uni.CurrentTerm - - - - -uni.Student - - -uni.Student - - - - - -uni.Enroll - - -uni.Enroll - - - - - -uni.Student->uni.Enroll - - - - -uni.StudentMajor - - -uni.StudentMajor - - - - - -uni.Student->uni.StudentMajor - - - - -uni.Example - - -uni.Example - - - - - -uni.Student->uni.Example - - - - -uni.Grade - - -uni.Grade - - - - - -uni.Enroll->uni.Grade - - - - -uni.Section->uni.Enroll - - - - -uni.Course - - -uni.Course - - - - - -uni.Course->uni.Section - - - - -uni.Department - - -uni.Department - - - - - -uni.Department->uni.StudentMajor - - - - -uni.Department->uni.Course - - - - -uni.LetterGrade - - -uni.LetterGrade - - - - - -uni.LetterGrade->uni.Grade - - - - diff --git a/docs/src/archive/images/data-engineering.png b/docs/src/archive/images/data-engineering.png deleted file mode 100644 index e038ac299a718fc6937b86256a3f95f822812dd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63773 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 diff --git a/docs/src/archive/images/data-science-after.png b/docs/src/archive/images/data-science-after.png deleted file mode 100644 index e4f824cabe945f6a75caf888ad2c9cc85f878664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38382 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

*QrkJe`i5(HrQ2g;A6D?b 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 diff --git a/docs/src/archive/images/diff-example3.png b/docs/src/archive/images/diff-example3.png deleted file mode 100644 index b4f511fece51c85040dcce5a3a1ff01da0aa6850..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14483 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 - -%3 - - - -`dimitri_university`.`course` - -`dimitri_university`.`course` - - - -`dimitri_university`.`section` - -`dimitri_university`.`section` - - - -`dimitri_university`.`course`->`dimitri_university`.`section` - - - - -`dimitri_university`.`current_term` - -`dimitri_university`.`current_term` - - - -`dimitri_university`.`department` - -`dimitri_university`.`department` - - - -`dimitri_university`.`department`->`dimitri_university`.`course` - - - - -`dimitri_university`.`student_major` - -`dimitri_university`.`student_major` - - - -`dimitri_university`.`department`->`dimitri_university`.`student_major` - - - - -`dimitri_university`.`enroll` - -`dimitri_university`.`enroll` - - - -`dimitri_university`.`grade` - -`dimitri_university`.`grade` - - - -`dimitri_university`.`enroll`->`dimitri_university`.`grade` - - - - -`dimitri_university`.`letter_grade` - -`dimitri_university`.`letter_grade` - - - -`dimitri_university`.`letter_grade`->`dimitri_university`.`grade` - - - - -`dimitri_university`.`section`->`dimitri_university`.`enroll` - - - - -`dimitri_university`.`student` - -`dimitri_university`.`student` - - - -`dimitri_university`.`student`->`dimitri_university`.`enroll` - - - - -`dimitri_university`.`student`->`dimitri_university`.`student_major` - - - - -`dimitri_university`.`term` - -`dimitri_university`.`term` - - - -`dimitri_university`.`term`->`dimitri_university`.`current_term` - - - - -`dimitri_university`.`term`->`dimitri_university`.`section` - - - - diff --git a/docs/src/archive/images/doc_1-1.png b/docs/src/archive/images/doc_1-1.png deleted file mode 100644 index 4f6f0fa0b5517a33b3bc07e8ecca77a67ffa5312..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3054 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 W65DbqcF2dN00dlHtz6YS{C@y=ppSL{ diff --git a/docs/src/archive/images/doc_many-1.png b/docs/src/archive/images/doc_many-1.png deleted file mode 100644 index 961a306dc4ead90805896f6dea06b286917fde59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5673 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 W3xCg6b-RBj!H-p*Dpx9+zx^+1m7!Pw diff --git a/docs/src/archive/images/doc_many-many.png b/docs/src/archive/images/doc_many-many.png deleted file mode 100644 index 3aa484dd6191a1244dcb38dae0ff72bc75cc1786..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6850 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 diff --git a/docs/src/archive/images/how-it-works.png b/docs/src/archive/images/how-it-works.png deleted file mode 100644 index 10c611f3d70e29202fc16f13256d8d64ffd99b0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109082 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 diff --git a/docs/src/archive/images/install-cmd-prompt.png b/docs/src/archive/images/install-cmd-prompt.png deleted file mode 100644 index 58c9fa964b84aa62b0dbed007a8ec8a88eb5f0bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20046 zcmbq*by!@@l0NP}5Zr=$0t7-}kO4w~zz`rvaEIW*2X}`A3khMc;O_1o+&y@3m*D&k z`R?A`XZO4N+gu=Ns!FJmq6{t;1r`DV0

UDP;r%#4rQ|Bzp`b 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

>)}W?%n5*01k>@Jt722f>ZIC^(2`<} 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 Nwdw z!_2&EyzjmD6Z?7I_xt1HI5=E0uDRAZSDoiCF2Y`^%99Y?CBnnQBT;xMqk)GXm3L)%1GZu#5--&T8)<;?c1K8LydWe9UGd%Pk4 zZney}PdY@QzXa}IIj6e%nh0b2fUD(J46NdHC-538PttM|yW4u6 z@%XoQqvsXe69f3%FAH@kBfiN$W81-Y0MA_^g_}7;m*Z*~p4EO-n|Iy9@L<2s5AQ;D zC!C&p9hQ*qq06li^9fD@VnA2T@ocrRc&g2>))>;xa*?MsVdVq%*S;xg_VOLKj)@R9X5UjKBP`C>(p0!8 z6%-?_=hEz+ozjJVKna>$CSs)(mVDigD6(H>)#C!d^Am9A{1boZA=GuP1wE-^pWd_? zq!zj>jV)!B@@_PD2q8;9sp^v-4QOz3!N9h+kq24nh_du(1OfNdNU%WHaIixC_ zHLhXx(!U;(y~8pO?AymQZubL~=AnitW*sxzyDf4*-CHo3Ot@l`g&&W1zBFyGTA zDT-SL4gH$PucV}FH@*ddZCVUA?&Ch)R{P=~J&Lteo7p?XH(*0Z!CdFqW2C6kn^x@$ zFdIojX@}rc=c;Qc_E82P7Ut*CgxDF0W z@x=ud$RkH8$F6-$m2c?Po(s&d4A;YJky25Are|R3^7)GY7BuA={@?Td{4J^K{6fIr zbANyA|N9~!NNf5tJMp}({kv5)8V5#Wu^6t)iwgvv9~TI+74av`?OzH=2jWkf_8ydI-9(xI*%bfDLiQpVrX$ zoq;Rx_)GkG?1~ZT5GG%OwB3^_b=je)2n2@B0WI^^xgL%q23-`rhXn;g9ef({`_ok> zw`|>4RyOg}xAH~8vfk38%PbHx!0yn-v)ZgSZwW&yVeM)-s;LJYC$4pP&9JVnLn+0X z^z-N9ny;MebQ_q%BUH{$ru+}y$F!Y%#2BB^2lS+}nK04ZhdnX7NV+<47c(SZc$riw zv^i$9UdI$J7Y2NdmXBa5?-3~$-$Z!zZg-EbVZF*y#Z8L!p}Bz66;?c;VPBwOqu7zR zA7Yi7TR|sAn8n(X#P>lulsuvfnQH~lJD~i#y}(k1Rfy1*q0hL7vkgO9UNC{ z-(Dd<&cQgeMBV7^`0V1zyHvad13ITze;(3}(x1s-SWDqaRMb2OH;|Mw zIlE=zG!^Z!@lrq<25b`hMJ{XzpV==2;N}!kc&U{4fR*4LiYBoutYNo$j%SMSp2b(K z*{gW)aPM-V`HZmUL`R<)7Slc7K)5ri#lYxlks%XBg%sxvlkT5I>H1z+;bf#=t*6n4nyCx5wANjIiC87)eg#TKpq4&2PGfjT`mv4kd=WUi-4^&~L&S%;> zC4SE#VUvxq>8dqht;4{~etV zRe$u+2VVP=;@a-D^@$S#dGGE&7wCV_f&XFot4VreN3eIfP+)t@gT{xl`^1HvdXB|C zp_G`RlS(MhgzrKG>OSLza8xgZs-pf8_`;Y7giFcuF{x_vTdfkamwPawtw#HWivXX* zdwUGX)_(T{rg}u_VlE?C^{(c8%Dlow$osgovqacf81<)<#I-I3%|+hxS0zn#qpKL1 zo~7A?a)gI-AuQ@?Q~jg$mc-uybt`U6;=sPB^s5gAkqj2~Zd6%@wt~dzU)HFbOtrC< z?8Zm;I7RYoA8{8+@E$EFjEi5`FpNF$Pbjp0aCYv@u72GJgm=?N;0~tWYM6|f;K^j( zUTovcr^>wGPTD6Hq zvCsS!(U%_i{K!wp^2sj6m==OierPzq#zDFi0XqI$QP0s`_3sJ$m&TRJ=HaBH)-f6hf}{UN>H4VHZa#Yr-D zMLU_>w<5(SrYM#5_e6STjaE!bX?+R1Cg|3-ea!t4BAtzu_eGdY?R0CyJxCTfc*LY| zK$_!iK}uo%3yf$Khg%2QIrlRAb~God;8a^6H}0G`TLT=D(9%9^$wM^$nz;FQ1A)d? znQA!FaFZift!0zKjrk5&$2-R@xsCv_ZFL}UL>md4)4bbYl=^{q7@RaWJ(s`BA@i=W zJ#WOfKKM1c+%Pr$E5!;jE3ci6Li{UWA6YdOkfVpPpkY7z2Q0lva!7*4<7IxcM7h3lZIJ}^0Y@cwj~FM9DOwcqT^Y+!iLC7x16?p5;LUkCqQ6TE}&v7 z^p$^XP_MEw@sm~D(Cpg~|6EG?J?WpyD7N1LYj=mzZW6mIwvS4|UiHN7+$>L>wJJdh zC=7fUz@*j9+*m%=-C9erd8DIhRC!-zY-u+G!mT?rReL#TvLD>30e>*|u_9Z*F7@jq zs{?;`kDqFgRG6X3aCOw$D#EBOw+HP``T7u*1l#74~hvWo&es4Xgrt}>I zAA9bhpoh($Rs=$`pA-E7^qLDOxDUV>Z8I(deW|rqnro|DXWqABed7f^OM7cVMThE> z`KQd39R^mM2+Y;8iYEO+y=j7+M0W)G#s=CZxcIi=3zAuP8vcwW0&*MM! znTWZtT6)g7Lc}a|6caznGTQqN)DxOBUF{cCNN={<=jRaj{;mV2=0TU<5Cn_a#HtAt z`yE41m^%gS`^`WEwG$jnP`$OUS>`&QyhrOfFO`DQzi;m)X`*42A{(HI(lzQh$<)Ug zwkq=e-yXA0a76$kA45o>`S`Fjzc_|s>l3yjPA)YlPkcCkJhB3bej)>ejnB3jB9cL? zDPUg#RT@?6Qo;@XgW8NH$b+WpO_E5kN^8%%vlmg1Opd=g5BZoFAK34_y9W$IYEazS zRcO^%%_iB>0y-Z;dhW1JdMUo>dl(M6-f0aqBXu>ciD2t_Nszg)m+nSfc=USe*H*y{ z|7+drz}%O*_P-Z~mxXa>=PCo@n2a2(9Z~j;fP&apt&`y=cur&x;|`t=+i?1L?+$tF zB|Om8KoV%ssf`lI5?{L|c0kgK!jpDQKl)O!^Fbeq`~r?zXlF+|GQOo8?j85{*tKRU zL&n#s*7H0*bgf@y2;gJ5b^ToTtDn0VhCJSkI_GkOPoF+-Ul8~Xes|QQyw4g7GUoRf;TKvqhzJwR;@jQDK0EhkG@kTMPa zkU!mFOZhS>Dec(hSq_Le;q2zj1J%zMNPNd*@5PMm2$^j^~}NbwG~ zrProgtPEKYE*0fF=5bGyka}{9Hp}QZYn#7?Cwe$^spdwU;4p>F$EQYL>BH9s+S4(? z3KOY6_H~6K0v;l%2vrTNKTC@(PCr+oOvVI}Z6YHL;!4cG24PuW3@+y$et3krMJ9;U z;5@Fd3dt%A`35=$Q@YnucIC|9Sb2QY@;!FqTzIXMIws|fzBOlF`MBcqxrCDb^zx`J zJz5}Yrmk((c?SUvT2?Ux57lSCFUOxo8n4B4nv^tt9`sEsJqdJRJ|;t{OEuW{uSoE* z=k6u_es%9v!e#|q(FIL?gY85AIrg_o)SDh76M~joY^=R$jURu7iCZ9zpwosig|^9b zhbpEun#P20kEN1SSaflqNq4*Bi zIRXX4J{l62oD_(E@RxfUpY8wlA`@yORr8s`{$zxGS;HWgUMu`tr=%In2m)ExJ=)Z5 zIV+*7SMtue3z{Bc?wdFxNibkzddypL2O7}XJ9O!$azjC;IN(eCtjT2AUfH;n0K;zn zqIy3~LL(^6V(V^dSxz0U4p3VFKuiGfj@aF=Q0yi8!OLd{nU!)6*uOqnGsEX-=~ue1c>ExYG0rh98|a>cO=)f*@RRo-h!ID#n0j z^1UtPVL$-{@r-xzTkXScRDIx9DED>N)!V0$nmc~GMwVM;rg{AQBN%E;(DP3saW9by z&N=5oH7Bpol$NgOlIZj=%^Y^m`6e2a9i7AyFR#n0J4rv3Zzx-IH$7+4ttf-Nyfc*y z(%gGlt{zr(c0R9XmUmkk^QH&HrkFmB&ry}^@aXNfauIGJMd+qCS%M0|R`i-Px4W?D zix`ueIb882a)!I&9;my@Ga75KM3u+r#}JkSwwzy}tRoj%PiWq>7t-C}ge^Fcd0ogpV^V6f^;ubjbo`O*A7K+0l0tP&#! z@>jLe1KrBU^s0Z4T99sa^msFg@FOMGc<9V+Z1qfy#C^riZwGZGzL9L`$U=i6YEuzKL#^=Bg)4Xq ziSOdhIu_z;rc=B-I-y!Hsv?it-OtjZTJhA*s zaW1|awM@~ByI8uW6TgD`yq0lT2~h7Z>@BP`-^A)u0{Ezw;TTxNfuTUMVHNQXcDnY5 zW?LX#SV@0_kni5__1B}(ePIUkBefzUo(fOJ-q}$o-CxFQd9$-TpV)%tPyh>+^w(gJ)nmKS- zzYY%w&9b^WHhdDGS8>J#z@UWzFGQTMQvwzP&;Z-ujhe+i?nxXCAoYg^c&MLml<-d) zfMso0E1p)+r+)3#xdFGPvuo()V#*Te)7n%SrX08pU0^*G^OlZw2HpCgVR%}bS+ zDn$Q)e$>A}KlMeQpkHZqOH!ego!JnIFQurviPw^Fz&dsI=3NuNWHe%Q z`=v6ux9kO$*Rtazp+qm5WyxARA>+>3Tj2?fqIsjOPo``vf^lIFe*AqWa+U_Fi-PyqNGj zHN%8rK-{jcLj?sZxMx%~WQl868kB4oJZA8R7RdoxYP>p)$-N#YV)VtPapN6JmBJHh z)Rq13k+i>BXgSW)Z8fA^8=f+rB0OSxFRF?zO1AN#ADNT2c)JK^es2a0z8w4j^jMqI z+IO}2CbGMO0lIuMdbbfXY{MUPeO*I46@^dm+i3ZW1yIJt>WJknqr$bLq9+9X?W~`_D=U0_HBspK2;l{Qts8=VdfuHzCbm(2cbf=SDFP&k4uQfa+!Opxo=! zEzV>MbfJF?sqAt>H%U4g?_9R=1gZdlJU;EhaLzKlZ-qACDzMtK>MvHlV+J~YBF^oD zn+Lj65vky*KBkEC{aI-_^Ep&zU(j{0VXAFGyHDI`)|N-U6U$W6NtwCBkI69={!Q4V zKk8(?m^H(|82>EO_ytQ+Lt%atkFWmo<~ohsv6~t%xiUIbhbuAo0}qcLtMa+7kpG~z z`v<(va}zP45mY{jh3Q3;vrYfvO|fD{+O%BB_Ry5rF-fcc!0j>~dK0C**0s2BgdnP( zG5cNM*p(N(&FZ01YILkQ$FkV;aj*R`8?~;HSUkKK%I|*SZ2Y9Ky3cO{eIvf2S_J5z z3;@$2D#*2%NXU+zeB78Vyxr1-lWB%0TqzMO5h_-P>w!~kL9ZExZqaw;S#4ZxAT=nt zxvImmpR-x^m23AW`!UftY35r2F_#}KSnqx=ZB1&A-qM*me&Ja%>wYz&gI+Tbb zK*>d}@B@nzk-h8((?+iS?ooUWMrVDt5<>HXO-J=@@OTZZG#7y%y-Mq@(Ws{AotK{VeCffu3glFctJoFE>lop+Fgw!ua%o zGBkl5PN*me-i(XSHy1RD$z?^}ducyzVreDiTlhTx7Z(#(7?a5mc#h1Pu?k2P3knho zEB;l%%HoPFzeU5HtlBs|zq0?lrj?sQXBTzKX}EyMS9ZV8S|Uk6nF zs-8<1XhMd$T+9GU<8*d8a)Wk5(QlWxg&O{WNtg=eA*?&Dxgay6x28FrFEg(W)nwxS zN}FjE*O_9Y-hp%vN%YjuWb?{4_xVoseApm$wx{^TC>95Ve|u|RHWN=C*FlGV@B*o9 z{U8u1%*4BgNk@{FraXVN+cW9g^ToB2 z3gb}f_N)y;lTKTfoUcD*Jdn+rWN_FX!9nry$OC&hQUkDO?J_|W{bn$9LK|8DBkK9t zWZyQQP@R<|5I+92KbzQuhwdZ83lSBi?<9XC^>#x_A+WQ@ zb~_oL9SGdg+ypEu;*zGn01Qm`ysU1ychG^l5;1(68Zrc7BKk~!f${5;ihYLh4>&H}${DH2uNglu=YCF>V_`w9s0qE}674b4 zq}+xYc!(Acz}9DumzaO*%O{G|`N=X;`f)Ai(ozrpwBXibY(6%j<<0A%HSO{dX8S-2 zQY4)o7tpP`t|NV^3r?3GkgkcJ@^~{}bBsDXilFp-x%)!FNct1QxtpI};aneEegnBq zNtZbt292`5tcP;VmP=XbEczHfOV}rgr(i8Cq*<#MO$>oZSJ0SAjC60m;H!7kzjOm1 z__4Gw9mg=T!gYos!b+G@RN@n$US*iK)I@Gsm1;7x);ZKl0DmZCo1)Y?9aea$oZh4zS_p_GjLMDqa9T=95T#^cU--q-Rrs~_xbobd<4 zRe!M09~cH!_TL-v@K<8sFLwm#n=wduFM$JY<&>>Y$%&k;guADA9zYzP&> zum>|ovQ0~FCyodjpahf@@xI{*t0etF?EaB-q^(GMFF zZ_)+Yz4h<(oqq{N=h|$D1NsE;rN1I}g8U`<<>4c|>-(t`EgvV~BQpU`7ng9(zmUA? zQWzVe98N@rivIMQ1bY|mf4J6q_gk6x>R}R?sm&X!DKQ>Uw+!#XCpGRoLa>+C{_M?o zc5$2N$-B{EbB#Cc>}x4NJZ&Tr7b|mTyi>c3Zm&gKvv?G0ww(gi?s@DTHj(O918GmP zIPDa4^Fmjt2C|{KQZUGIhLSeFSQ;o|Pa;fbb0RriiFR;?!zU8cTXTE7!Py_ceil5VONs9fQO!EgtUJq^c zdnTvtuT-pUU#Hv;@p3HFC!AXrM;Xd(VPy{~@56EN$8{H!A&uD}_uxjutE%NN@? zP!6WrFtCI7Hrw4+xLj=@?rZ9~cuh7bB#kX&TFn};qJpM;quh91o3)(w%26BNB!!fC z`uF_%4ek7dhZA&_exiBkqI?@Itkn}g%DSCn_h5*|n8sy-?a!pkgq01qYVmSzGxR%< zNlL&tW@T~=pYT!E`NYoD`lLR2c+Rte@42fwGL|)}EKV=0zHV&24o&O={#O=`BA99V*BUGb~irw^CdX@@)AqlT_{vKOuT&r zqxu!elvsWfJX$5HdpV^qQUk7^6DW(kCP%g$ey}MYxibiwc>h_ZbIV$x4;@0XO={=h z8Ej9u+WF0MAPlYg`n-c&v)1OWx{|jyZVVZ4jDUgb#4sNEaveeT96y{kUGQ+z(TgVE zrA15Tvmwx! zZ`OK0PEnE6ZI~=fC8>%lVCRe{%o;d{>5rrbqPJDqwKLAQ>0={JuZ8ukt)02c7PAZp zsdy-qHN8@u_IM^k@l=z4%}+qh`z=uV;nj811H5ZQv{4lI`GkzEF!tyLdA;R3l_Viq zL?ZHx^eQEz567D(C~Dm^ zNqvP>$0#nTIevqTk{Qv>k$1}r=5Z^%_zo?I!^>G5kfIizWzaU~e^DnKyeD zdWUls*1^q$p7aI(!qvbuV}mwb_l#S)SCLsG5=Qf_!M$;RlG@=)`^URJ#P`fDS{2X! z6S6jph?8(+~EFT>jEy^^ooO!(NKjCMGZd1cw2q8&N1(Nfi$L{~ubx=>^~ zz&`xzpGBJLwbX~2Ss*cTn)GAT%iwG8AlxXPSU(-}t-@L#cpmwWU7XD@;3BPJ^MdeYg=*8^>F~7I5CRu!4See$Fo<(OawmnV)MZDmYr5?%zM@BwW11#$qqgU3juI-d$`&@FWT1P|bF zbnDY);H_kr!)_;2++B$mL2+PI*O;f*0xBK!qXW?Yx&>6{kE5E$k}+Qb<{h|!o95r2 zfxHugD?v}9gUjaNYhE+>+psw|VL&LnRg&H`Fu;+N$Q|Gh!0K15osPRPtBP7pYQrdQ zp3{!lW+ytJ1(R-HXp=Ye@(f{ugxkVP=reYL9C#yZcQperqUFr7A=BDZ^np&6O@gP* zdTj_(%`Llt#KQEOXS!oQGIcUaqu~oOT?l6`Q)NL-nqme$(%oVTZ>i zEY6fTV60LB=(yc2!8k3tn$w}<<<abJkCu$^9UvpY z3TfO*RYu2c6w{9q!qGTy1F;P}LUMGbci&-Q7_KA=^?WBfbNEx;+oAMCY)|Y>?yvuf zwWdf;%A7r;Ctu)Kd?=9NjTPXQF%l_yViAe+Y<{T5FLuPiofV=jeDq98;L4i zzwY*CPPKig)MP;(L&9;$)M&4Iz`d=^%5)B_^|si5$ly_fLivsMh8HbviRq?4{4cXG zhzeewwRX39c-4h~m}tn4(74hL{Ur~BbLeXr>Li(2V3MPQtjO6-cXNH0Y8W|-An+yj zRok0BO6d8|@Y`A5QOvuMX96(V?D!2^(T^GlmDq{*qi&yI8O4zfhF;ufDV~N%*$CtY zY`q;j8VNh`w$%AO1k-sqHPJ0<%d_YXTL1V&0>fPW9}u{#6EKk^!VQ3J!y(z9<*?SW zL#e{)H}8-j(O!i2tx<6)#VhQ+v~$9U^|80*~^%nqizMBSW8BtLklp=3|;8u2@z^xKR-JsVyeDceL(T3`Z6zRBI=}iJ9l7`O zMkr`SQe~pPk+)x=OFNNtRd_q^mXTyyV)$DJez*q+KgXU6ptSt13a2#<(Au61??1!l zJ{6Y!BHVMK)F69YJ=+QIis0GogA!N~+<5CEBH;cABbXZ*nKuxQg!*O9kE#q8(T`|9 z6EdMbOX-DwsSpTT)9w>XTTXU;ZogCzE~a&Nl^$a0=p`$n8KCh|p0ts8;z7Yc;@sz= z$-%Pu0uL(~0QY64*R{$!vTR+5m1OH}guAQjA8uQxgxpAVaQbPes;|Brr#7Z0)!z!v zKfeAlh24=x){?KjuSxATsKk|94U)7@kKT%)+ z(!75GNyqONnTR)&KY<#&H{s>_zwMt{7ABWl9f@Px;8o*5eKwbciIZxs z8laH;!F-R|7F?X~%FG=fGYBdb7cMHZ4d#_f3D;odM|eHbL5-&tcFjm)2iGQVPCO`4 z+7E2E`k>D?K9wxMEOdslxW&#c<;TJL3Oh9=&Mn(p2%^+Ow7wSDw*Bi)pWgV z6IpzM$J9m~>AGF;#z2*5)F#6%0=zc1KiC~!ClCMii9N%A zu7VfEXzVAeh5^)n?>FiBW1k7I`FFbvYt^v|&=zC<+oEDG`9+2XFLga=jn2KFR@Aix zU)Y3Ny3F-TmTY7>JlVCThuN{Wdwkp$>tGic6ssH}-gS*M6VNr177LT$+;zX4hm1SwLf6`De}sOy2bfW>ZleXcUwy{6w(#<-hZVRN6{t?q2djZ@fR ziEE&jUs_#5mj`6h*vV@*+*v$9$UqDYa*M-IloC3SP%lwEZP>8_>@-!RtZiXFS~N?N z^O~$FiT_aJf!z>bo@_-^b-Prf%dRMjY*?FC1Eld3PK-aNul&}+uo}_$2iBJ^a;i=~ zIFZcwry7s+JiA%w*x3(RP_cOGtfSv=ed8y0e2H6Tbor;Pk{y5WljQ;IQ?8zlZT0VltesW}_V`4LlrT;l#dGYr5g(ng)osnm z`jXRa{ClLfO_ChJQb2`=9w+{Ttmi+x{6G9U{+i1hMC>n}yV?Bfk3xT>nS?UgCY(8Z zWBg(H4PkP_=?XF;15DdWYrj~_KHzg4HtW#hM0DyYmlu zZJp8NKQWk2yT&4O^dx)RGtu_Qr2N&S&r`NlT8g0qJ=7xR>nSKTm~6qxiwn?(?2lIB zz;LtQZohYDz1gzNyRK)jW|lxYXdsKWvr-OH`s0O%^^&g4IKNgMyb@zAIs+rw*V>!I z>FNx;wj>Oh$D>VQ9B48KNbcqwf6)EnAIY6$@E?-9@I1}Bouui^4Rl99d^ET3vn3Pr z{$8(!;sU!jhhaXN!m_1~%DPvk{7!ttR92jnrjlij3N7vp9VpXfHLeeqf1n3^SR)v1 zt*3rjU&?q$;a9oo(X&fXt!gTW}2#csst0&tyOEGAFU1Ks8YS9}60kKfYO%jo!M*@`pN*W|U(kQTNf7|d-Mdg@X zuGGFv`ssLzDl@xjr(pc*N6Y*|Q(Nn?AaUZJQ|?;d zYhQ-jHp~q9q|hG$?7E(EJNBa^*T4YL7HI8XgBEvQh88WoH=-pn!jvf=jfH-$)T|HW^0Iex76 zoGh)7!|iT)O&=SXFrl`}hdxuqQSyToR@B4*ivkZcj`dWDEjV-p>7t1x=tpeXHd|-5 z#gu`~-`v>d!ex#{823pWPx(2`oa7K$@pQ;@F6dZ!Ne3fl) z-`NMmhq!lxRpMBu(+foV3jJ%fExTA`Ke|wT@HD@1nor|;Ry#{Iqp5rIExc5!{U zcL|?tEh{K)e|K1VG~1lcZ5m3g1KR6H{ss2~xG%_P)bc4re}|Cd5p-bdVnh7p+5vY$ zva5XfC&ldMo-=CPy!u(BI$wo7UcN?l{PW}WEfNG61Uf8Au1Xjl`v{%xRPG$!3K@mk zd9Z~upsdl#H(yXy95Q)P(vhTTe>tR+9<#2HBKD7T&;nb^lcMNcFT@`x8(HS~-O5{% zkE2-2Os{YkE2OF9j}}V|!A9=Fw=GAn!cN`B;TytV;!ZjOx1t%wwr+4AZ|ksqXbynW zV&2E!NQ+3M`}`^_e_#|Yq;^#si11P#ZNZorug#ysRF`#etcBQU{X zH?oDbxLPF7p3vDBd-K+T4^YGecf#TB_>2qh>6&r|-n)c}+Ro zvZJ+z5_0um)Yx6w=U9yAQtjutAyn6jW%tsaCDz{|E$);;^~^0QzkL~`A$~NSidxp) zV2B7XKF-DUFps0C_Xu~K154Au)22r=@7hHs9@mjL{2P!VCHsm zUKTLSUYu>ceSvvdy?32GZ zBTc~IDF}4^%*-EpSXD${82h|u?i9eXv zamc+bNOjV|m*w#;9~7$>m+!JYLTx;48J4jS6SY~baaGpI!Ii0!6bH3rCluGU56-wv zN0tM`u6F?a6#ybTmvm~?ytiu))cy{doP(#OUE@L*GlBgpVIG+}Zj&Q9${xuSE7ArS z&ggaO`cUdTn^b4iRQJ(qqUzVdbtM96{2j%=T6s5Vr&sTESd}!qcc<{yvv3aQ|FOdr zcfqIMUqvcpis%=6HApABO=4(%wV=!)(=*{M^39#LUUR`H|)2fzVrx|8fqor>Uvy@|>X zv(!Ak${K>J-$lH*|5d!hVoHtPBvsqI8RwyKS=4lP{B5#S{q_4d>9ioZ#5rY7Wb*ir zt0L;r{nD2>iH_oXv>>CVz-z$-<<^J)*Ge9*B?j0rt|Fe2R2TGq(7mJnoqFpuq3fo2 z_GxQ2_cuAxOHnD#v62l+t+^WOz~Mfls!v z9d*W?O@@YqI&^P7U%1=4yJE!Hfx)*#x=CH?ic>LGxJOqr?VW?6c$i0=QrG{?61 zT>brSb@nt#b1)t313AQ4oZq-Wz*}b^KlF1!o0=;X9}Z^`=k8YzOTXA-f8}*))`L-i zgiQ=yKO+a8dv1Xf)j}V&*yg{8pDWuOWd=N`uxCLa+n@iv{PvD)iCAu+*zL-`oX((- zY5Scemjt+ZL*3u1)Hz^#Ig4+MkFa!0_3YJNH1@s!%jx=?>$sx(e|Eb5p^5Fh=lXo@ zPtff$>jI>NkDx66!Dl7U9~~rabFTy|L>yH!mN~q~n1TA`6IGJaNdD}*PDz^%pErg5 zt0GYrUu=N_`mL2!?7!2@jw=60%?w9bnb>iQV@4FZDB#sR}pI zJ&|(+ELu$Z-iv#THJN?KsZy5DEgW4j;)XZb?hqGpn^W#>cs(dNudH;oN4Pdxo6?a) zGImohk3<7DN=V7g2Voz_V6m|%V)}91XxIKJmbJnEwOE$=t5{xsWJaK$m9Y%LA znPO0@_CDQ^Pu*?JsJC_z%B`Cn{F$}+YvgB}tcn+swMKCkuT}U`Gq2G8Kh?uhz@)}A z8N_K_@MP3Dm$;fRYW29Bl>PPul-wK)#GNe0K68ks!|XhHpcB0k>w5 zp$#0HCN|-OtSi|6ZuBZTJ-<0CC0?1&`ho0tO*#iAI5S`PK@79eq_Y_BWi*HWr1c{} zAcV4mvnTUqrhWgK>9j|MfV~Zd7U?GNuUvcQ)3N>z%5*WNJ-xO0wo`5QI2mLj^zAt7 zGNBW?A&t#vYT$3sRMrmqnDV1ejCI`g>1l6Mt-r|VIIn^wwX6i`pdON>exEFz@}x%v_I~c8^H{G|NUk_pO!51b(vZt ztaebUx-n+iFfxPxo?P&_-1I#bjgm*#n}9QW%OT^icwU(YI#NjGEyW|p@NFXV25K&z z;O_e)&=WpQXXY!OEyg@qtRmHJzhL^1ET(9dkiTo!LjRVqfVr*CvI*#V2#3*K_;P~_hb6|j78@*ST%g8BCd_(KPr@-K-(4TxO0nHnxj(?)E`i__8pFZ%?WS* z%KvWmmpB28o7JOOS6iA^4viIpfC*$i?4Y*BvaF*kBzttqPlEBX1o`3)u|3nwp*oYl zYMbvt-FnwhDb@kKjL6yEhZMJncQqCZBrlBN14J`Glpp5kOcak8n3|0tfO}|*tnn-!9|9&* z$U~W}lwa-$de&f%WT>+{(hCf&tc`u$AeThx=EJb%L~6YKoIh5!?>j{bLN$Pu4Q^wW zW=AzkyYg>Nk?E|}{H*n!w9ZsRTEwde9&%9#7HMrP`xuM58H51-HU3XT$9IM+@=u_x zEb}_Pa~kYZ))JlILVA>4gklwAdXCgqXy1~uZPEOV(Ko5)3TdykeH@HjsAayCUB^A+5hNs)?-m?wpB%twxuTRdvvA34_qaj*4RD0%e$#nyEM|{ktJ3z z!Hq1~i*+`Z0h`6gg%@$(>vtMgqigUH;h`RzwrJT7xT-kyngI__qzmx59fs)mKb82^ zT0Z#c^Xg9*OoctUKbP&40}?5`?2q-cH2nn}6+`I-Bh-gfWM z@DCJ5e0#F>ihWQR(?9Ecmc|YokYgT*D)b+=84+73vJGoc*w#s%f_@x#lg(B`DlAh+ zO?ud_Ni2Q%sIQid9(baBNJ_Sz74+q$yi&&z@`KH?LZ|)lkl{wC>!J^6dV+a1*+pELaRX&-d7JUoxU1pg@gy!@>v_h zB8e6=zZPh$S?THLFXcnEzeiP;yxg+wA7P$ zWOUY%e0>)Xq-+_T52cW-X)Sadf1h7FP!Y?S3hdVsF#kCW=IK*F!{AIo+3^*70wcP9 zDUG!PRbax@+w80rI?FM7yTpRp5w*UU`kme>GPwAd3L32vDj=2{{0uUx)=|wjr}W{r zyUd`^!>v|#rqtW~X;Gwag^lROb<@V|NMF}De2U{N`)c3IDqBU1iUF`~eQZY`oqC`emNb-e*>A-SiF~UdcYrMF;nj zhsWcX_}y||l$4>Lw(38acx~doiwVw6D)f3N=gOC5eQ0va5~}KKC_XliVzbG$LTBAE z>9LT!!QyG6wBwnxeYymZgMny`7X|O?m{cFXU(gB1+VSiAJ3fuvat@w()*9$SbB%4 ziK)L>dgheBS$bZ8rN6my1k@9kg%9uLY!(WBK2F{uHiKpfR<8IIo0D`m=_t$64?lY- z)a;zMnk)FLB9gOxvQn+FUUO~~%4t3fv8Fmb?zKOjzH2;#Pug>Sv4jnD z`&-hHOeg2H#XNW_8Lx1Yx$YO6Man$!r?)>Jb70eh`qKXmkq=({W7umAKcVG@44@Dz z@ZOmnfq>Lz;po%r2E8%ssn8Cj+efv6C2F{e7f{(>2Tobwj@(U1K(0Xgpngq#t>zLw z107$GZ_jUDg>;`=7E6Vi9DjF<^fS!3G`si$a=L=ogoitY_v7oK3jO_`mnS1e=`Z(w zWTCH2KEN>4@IXtk?`uL#Gn(&c(PQTc@Vu!0cPioQV%YL^dOX@H4%PiUYfowpM&z(VQ~MJS?Bg*LUAO>6jL`e!eZfyl8R6ui@llVbgW$pNGfNhkX?d zy+@MD+&3Yc5H%35YV)!tO>gCER}`85R*36FzM<t4x(|oEQy`47at5Shh+kz>2-_NPfNh-z<7{|0~?^^8E{LSm*u)H>P`= zp{-B~B4&HSP>d~zRmxK3voysREQK7LwoQx)5G~DrFZeO_71b5Cgrg}fP%y>W`%fJ@ zL?u@e1i6=4>$8@L{<7u4#zFtHE6)yBCeGphJ_P(tW7M^cbr1w{gW^@wM)B|>X*x1d zN!Fs(1;6ZXf`0IG+Nk`|Jqu*Gn%kMD@o9-EWfdT+sz~E^e{@Gvsm@Z+OirBKJzb{% zMQJnf$VF$&KKz>DF;9|$QlH#!NbQ7261UQEchj%K2-3we=gF2E0DVprUCVj0Hx-~-FjRklRWIVU6r;ejxSV>bb!-qO* zOA22{lRXy366@9HG$N_juM!p&s1N9_uu5R8dzd&yxS|f2w}pZ0uoKX@zNBx=`ExcIf+?#qeEgwH46+&Qdpz@>tsr{(Xxs z{f?zc_a5-%Z)$&fmd`uHG)a9sEI+dbbMy3Yn+1!4%!J(SI@JSfwBG1=F7Ygz_k!cd z)bA;_z3+2Ic`U-_8(P{b=A*k!*1hZ>Spm)CzKgd;uFelxl@_GE^&7x3;CF#U6|E{h zDodZ~ThJf1$#zF8Jdz^7ZC8+lg6k$E2{*M8k^!_j&+O1i?k?dScLJWkm5w3>#?D;8!Pb1Za{V^oM$Wrsn^hnVBjL#F>!xFL*xt(1_jEcMC?GI8FfdNmKk>o#ZlR zC+1>^bHSFQ0jtK&(rC(ug2CHbODH zCi#HfjTOLiD#MEx?QDea{Sq%R0n-oj`xJF(%By>Tyf1itj9sc5PWL8g;L(K;tx zwF&AC`X!(_IHcmvo8jN@p<`{LUe$5?wYSf zi!)Wn6^Ir^W(FZ>)d!gD^CBx9s*5+yahujaI`yPj(Ucx7R$XZi7y5*&UzF``acPA4 z%-#H^)C?3m;lyGG6pp5gjgEpXl}g7AIuEO4KD^7g&o^k%++&p-jOV=pCLvs^B#%H%d(IREV58gCN#3t=h!gj_A+n%X+hnD89G9b2}}VP`a6Yf50>h z5$Q3KV;IQV%=NI*4dJ%K?uShepRLF1yLMf4G?19($|~IWGUFfog3)Tan2>Df7W|+K zh#$VU(|<$caHKKQ0UVBs$in~3;dtB$P&iJn^E0f_(-FY4_`vFS|7Ol5++)QhpDEV} z1BSSP1hpX2<)o)Cbo&@NVm-1C%)!BVIBNxKEc>z(zM)-v<|L`)4Ryr+1$dS0e_(e! z{z{@eE9Z>cU(vanmUU!xv2P*8OrhdzT!B?*a@IDrb&&zEEPdfsgUf^e>Qq z-h=+Or4(@@_|G!$pq3eU2ihbSws_oi&zY<|oZ_TKScEt46SzzXP!P#s_Wo{9yiO#N zKm02qny=7nNctA*B{pq`mc9X^I-2Y%MU+)`UqDgZ9fmr@GXvH*nU5E_Q4NJU34z!U z!wQAZe$+D)l}{KsvamvWnp|bhjW-0WvDdnX6l{&ME^_Q%+^EsN@@)2DgjWnuGfh3M zfjdf;YPrgSS_T&|N_R#$t}UJVWm60@{&-o9le`r{YnUQGMnZibDGig}t5nw?dz8|D z%00E-fJL0uilUEHquJU$&Mg}zkKPH}9>QUam^6VSi6|@vELXjwX_UWe$rulX5{u59 zF#t4^l`(byu}T0Ng6v|p42OAEj6puSIpr$OdTC4v9#4He`k487T=Lt|QF>0Mf7kY! z)+bPfUsya|OY*1neHM>3P`xFcDllELKM`6;_1MrT8=|$?HEZUt(6cPm$X`}*t9zRs znNOa(dMsEPUSQdJewpedth9R}SIIS7acy+^ zr*&2Af5eY${Ejh+R%9AKJ$`dPEPs>~3qdRdGN~hJ{*Ey@--HAB65TqQUYog$@{c&Z zB0wox6_zi+ww(f<^=UW)0i)o0zJGMN7b)d3XRh@AZLO@Q^L{S7#>xuMij}xE_TIPn zkr^fpE<5+|3?Q$*Zu`#qMmOMy5{^4LKV@M+O)wlt{}+WMdL432m;!Bp5(?EObKKkP z{6yK4QN@4Y)EWgc4*bBTOF1+nFouKEbEa;AZ*TeUst#X;n(2xUGZ2cgp(mQk4-18} zHf9eu75H4Djo&YN3kbKdpVZ0VH5+f8DeM02jPf5@<-2uCjX{cCBve`JMZAfStIj3& zY8~hB7lrSMoj*iTZHtJAoZ$uVjK9k%TK{iFEhE|{Pr4@N+@jhBP>i0}2}~4Fc9sL+ z-Orc_F)X0w7iVe#5tC$HRvXAdRX|TE?AVWd&N-97?(c2~0D!rDUhIa_MI`H)X`rP8 zJaqss$$!AF{w}NhM!J1}txPfbZ}$_d7uA=F{ky{@ptF3ZO$*@95tq|@r9tFp+OxN3 zu!uXBpt)snLJuexu$><%044+GlZc(NHr4vG#)|-mZjjvvb>Y!VXNH!sW648#cyH43 z?UNpl8uddNg+cJ@vEeG=d>cM|_VwZX+*^+oH{CgGx?n2;adhM2y`Bu8m)$S26))#q z_5~jPT+#uP#ffJPv=?l3C<1Zfn8=+C%x{q3`+C+F?N+MQI9CBYmKX=6R;bB=IymB! zNh3Fra!h8*hyG0I-O?*N-x#4k4#2@db6L#(e*10dERC^v0pkFnEQTffv0`SRBy&ju zAPv2agwq5*oWe+-$-ZOTebz>@D}Izo|0X{5Zyo^h&~y|fGRq-xt(s>;2rA1X1kMxZ97)p7hqJ()*Y{0~;E zg%6LF?u6`I9gftfb|RI0UL%sf<<(ea7nb_@B@u{VxB%2})tvc24cCI=ynW2G#*pH9 z?QTemq34~X!$&6Qvs1FGEC*Ro_ld9t4I#}p-6N;%!%PClGE(^|pL9ffz-$IAJtL+w zdc%3UYGc(GQs{8E$_G5H9%dT!Mj9WH-zL*Dp_G`tDyn>o6FE$}E|@l>9M147rZ9e? zRMEb3sgzA_dvL40J`j896l#mpJBcyZA-feBwqR0W4L7&$P}qq7Iq4f;u7wl5`sG{&+TFCB88LcafT3M;p z0$JI5#hIIbEUh5(L3VP^Se3c;cEzY7oSr)hw|?GLV-ofKrMAqXk6xwKolI`}!$tbB z)6HZyjsDG3J{(OSTeb!3fA(xORhkvtvljinC!z&X>&O7_eqg8bQ1`~?xy$*YYf>qv=6h#WmTF?xt;PUw6PI7N+nh{&FGz-03K@EDGE0-KNS`u;54)qQyYRyLNsANfTU+1nzmChV z3=9#Zw|Fe-XS&o2Agb|@5~P~YD<2;o0|4m+z6F$GA!{BvUl_)J_sT{ETKD{OHl_crOoRSj&*VoLI(n}@&zBjtTz5|4c*8|GTBNG4ruprC3LU4cP*^i} ze@N7(P+2)K9mTr~G}ddrF(l3Ad-)q~)eTxn=tjIjc4YxZkVe~Cr{-;s0em$%fRT{A z3=mQ-0vg|FH^Z0@hmoTruqM^)t|CZ4&Tnf{x93|vgQGmxfu0(Op2$*hM!jtdmjYc8 zj^S|jZ5+Dz5(8t7IF)Q1L2X+8hN{#Y9R_%=0W*N#*+<^a(Wx?Lk4T%BZv*j#Gm^J} zL>OQjfC~MwI1vG>w@&;`0Uj|_XmKeV`P-nti@CtDeGrzJ`T*ZJWk?(<>7VhhE+GE3zW!JIix=M5c35*h=-86y?ZHea7Z;^jOJ29hfguQw}&M8oUTYDTy;jUJ=|i@`fT$7N+Z_oMVVo&mJ#l zA{8ZY@T7M;tNt2NpFz`8a`qHENh>xM_8o4f2Dkn73Y=T0`jgujxM*{td%v?^ARzn2 z`;Y8b%)hc<4rN~cSNyc59+-rR+HN#(c=O(rz%tI`<&J|f!POv6xnI5wRyRy7;0Gu&MCs*7-m+g!tq!RkIc zrc{;RJ!v5UH?Kw$6PSM&&=--tf8@Rfs&k0BuU=yAt045xa)n|{B$Y)hgH7j#w8#!XZ-lY*dfFCnTm)7@`oP> zY6$dlpc-0%jK`MpB!Af*!Y4*0Ub{j($FBxJMzB{X5mszg{rPr@=!A&mcKum4?fL>5 zfS$XX41j5#9h`pjvdT7@T&1FB#gAwIqK(%KZsR zKJ!mFIAFFsfuD}qOthGDha#ys_X15+VFyTy8+~X_^s&0lJIvh(TQKU1OB1sG>)bAM z1%--2I`6RzIq-%wZq|5E_yJy=ujcX2kf9uF-rept7q6Z3F_TiM z$2e(W_ijEaQl~h@DQ#6o+z|CmP(g^5#7frvRB1bilPo=N9GvdpM4=2mtoZCv;!l#t zd~c?J0O(*IxmBpUZeg^^O{~$Vp9G@ zH==+r8evGm7I_v)>dIgGp^BSUNb3Hk4DZ?EigO0*IVpF79=To9!Nf+~=>HJ1aEn!r z`KI9)+1^hcey8zXCz^}{p|Sr#=UHN$QVbjUG9@0m@5JS%!o236N4MZWpr116Yr^qy zcM!O3VwlF{U8l~%1WRQ$nIg{txXt>%m;;|Hv->;1GCC6CGG*9!(>Vd!qvq|8VLrL~O8 zyT%kf>ncPp%dV-?S}Ucc0i;0me=pk9A;|A&CD+Dn)E1hc zeaW9R&{2AM``l`)9|2`~^ns*O3CoR_!CU97ar1p|0a*T=6Ort)e@zT#uKkyM!{G6k zeDikO0yPEu2}z_c$j0B8Rs3SDGyn3ALO~n)3TE9#P22wsW!%mOm68xyvnrFGKgAjD z`3(cV6~3bU;N;RLUY%T4mSRi`!XFvRA>mY zlDLbGin|-=$e@YCr3G&Hy_&Oe-IQpVV)~+fn}IR(g?H`I2hyoYI`=}!Bv)ncWrrqO zzLY)TigT_Q<=MznFy8HcCb?kE(6M4;F!@LGdf+=sR3F;RnT^X1m_|luzDXvb?*lFV z|0=}H1rU{(QXxFk%&8OQDKlDabbPyU!`|t)6w}P9^qb(i*pO6WhB>dLp`AUGXuL1J z^H|fYPv;ivgtwm8{dLh$n(6$9>7O6?@MfVtJPOjze25K^=b*aAW_JCsic#vvUusJs zq21)YqJ|Xtc}m^&x5w+VceBHsKfE7$WVcu!BxXOFa-J_@KTrm?^SWFAJ-$Dg{+v|s z&r|dEj?_zh_ucKs4YP`~ry{rL=d0N^Kj9zM)^t!J|!HfJT6913nFR+_vz?GVq{`mTefu!Lj z*#ufvRxW>3fV>(V14b~=)zQxfm6Wm;%bAj|AM}*P=!nflmMwfCNyLxW@uof?rIe78 zUCdQoGEviGy*GlUw__Wv8L*}H#m%SqCyb4Z zzZ!n<<($^BrEhuyAXrqZ=an;aYjaCXs^n*=7#on}K2N=Ph-yjAW(DHw5%VAYX<$tN zUjKfBfZrV0@NW{Q%^3v5X3lp?Ke3$&LXtm!X>bw8k>=6j4Zj;D{>t|;XIb_zr32Dm*#(=0>=!j zV=qnZM!fXdn!qT4mPThep?ttdU!}eFx*N6~%gJP?7B>^cYhoN0$x#9jzS`O5bZB9L z)^Z(7eOzG6yy^4FWAAI7O$}16f#gKF5GKr+df(Y&~^EA{<%?gm^v(tZzQp4I$E zOJ4SUVpBqAd@b+^v3hWH~yp*}56 z+F$gEr-Yi}didjy(W=9fMP$mn{i9`#{HtaCe}P^V)W?>R8I%)^AY{${zScmhRxF*X%tX&<$OYwu z1DYY)VY7};dC9%6gghzbm^w_7M#H*I<6{gsvRb-^$su@)Eo&BMpFK~2o#=lc%Bh736)M-kI9xt|0+cBGvz~Z?d;LWb~ zyrQIRr&+?V{vC9%yn13$o7g~9R`*o}`R*t;Q*G-v6vZwNTM|3JQmKnA{uFH!{{Mh- zwc}CJoAx@c^A6O3&4KFLcfF5DzuPNbVItdc`SM-cfQSm3R^?i}kXvEBDE+sujH~L6}1b9%oG=nbm$)QxKWh@;mstDO+Rq`=T?SYP348ltahu#kig}` zrzfg^J9MwO8r@w(#{ZtvX@Y<`z0kj{pmvkPly{O-Lr6_{>J4HrG}ZiMpIOlEjckj+ zgHiIgi>Rs}r3PuV2ROF|z?&j7XV}>HI#5BzDEGA5`)nJ%K};yZJ|FTy}ea#K4-uh2!Y#5?M1d9@aPmMQ)~^II;k2ku!Wi>pS=xha;qN z=aTD=jka8YvoAeBWc9>(s{QgM#?10w6JutJ182InIEb+V>En3wQ3FImvTx|vf)Q~t z-9dlr_N6=(5;)UL^~)RJ&Yf|&F!I(1d`kJ$L!lz4Ct_{x75LUQ4r0yf`y0%v zQSLcm^wSyI*cCU$#+!26b!&RFM&9Whkwqg4fJUM?MH!EBHtDfuZjE@fu*qR{U;jm1 zZ>ygxtsRTDD*%pBmM=}UC03SsMDak0A$~S)-*Kw$Z$N9q)VZCiFwri6H!8UN?yWa! zO?ICs8{pEKXTN^$NGFinUl^h9U8;tt*8X?-V1j+>G_Hf&12g@4PwSlMSt5jn4S)FLRjVufTRN+KbK3u6n8YQ^r&HXrmubE-G{oC5}WI z`^C*97iZV5&gFXt2UwV84zpQtf7mafLfg8ZNrjU#QJfu4VBveuR$#d>;D%!9xs?7N z_fe7eD6e$&>pr)A+WeuO?5HGfuRo+((a1>T;0uyA??ixAfPn%SrzM~Ug||{|`Ee0xZNt_fRp_7CG6m?bCEWOr6pyf3 z5jJVr)Tq9MHZK2ZcAq<`H1{Ik51qH3T)V<*ms^={553%QD~0Wp(c?kygDd{0@%0a0 zEF?A0xVy*L%}jq#7|1!c5hUEKc?NltqG-M^qxN%OU^CmHaF*xEw?`x6xdgiLxg)o! zRy}yfGd8N8@aT^xaEfJR-YQvxw19ABX!=z6JF=K3M`ASf4IernO~R(y`JM0WEBe=| zW#QFaAwG_Ak=U$7ovF%|0SP^=a8h2#jEQ7O&3oi(~mk7~9~KeYjsP)SM&I zax>-_T{~9Go@i^l`L48=e*A2GACDGI+R!Nnr-*QZ^QXEL$mNsNr?(=n4ka~n2BC}T zZIlH|K3os?5L0?!5Ed6Bu$=O8OQB8edaufd@T)n8b@SqsZ0H;bWw$u>8u4AF&oW~( z=_i`5%J*xy0{GtgRpvC+g%!sZ<<+I@@UI=5x4#7LnQ?jSr_K#am+vRupnF{Jc<=4$6}QD2J3`EV3mGZ z(2=#)10S+vjiAfEaKS-c8csZ`yrZ{pZAaGbk zzAQun->dZtf148w7TWgob1^`cZW$(Xn6yD2O*(eWWH(hRi2`6gS3*qc}!o_yyQM{ zQJ3wL$v`6EtsZHR~O7~VF?SV}^<-z`vnOvFH%$MQvE@EY&9^CGKTql^so zW`J_7iKZ5w{Iw1uV{2J?ZC6FE+otsmFM9i88;^dzl!K8`PgP-?d|d_$5_i{^VRJs& zB0K%FQ1P4;PPK!BdmBk_<9b>*lz%wyD>tHHrQv}Inx#K1uWM1-QOvSG1`VKWV0-nA zAUpBv@Xo`JQI9UleGvZPDE^uHkSfR;-pKGXYn@&Fi`9AZo~zZ1A{oEdUfs~sbAU6M zAXHv9AZ#2HvrBF)vw^B-dnyGvVkJ_<3v=SNo`U#-Gz|=rORq~FYKVE2Y-4MIQPkN!DdJPu>~wT|GCVM&sHFJro>dl^#@b#?+lRn7!xQ^8X-9o z^R4%DR;C-D`h&YJa-1jW$y=&Uc^3SDCupQGOlP^s|4AywG^i@Gaix?2!Uo&pd*gDE zscEf{RQV8T@G^i`h(6h1@q5s*53B8{A4r~JLS0wi-Xof`X`uf~u9FI1*wXnFMVF|l zmCsYRYDT|mazD0J+;*^FFGuFJdSozY<=5xDH~MNpoq!9Oh6aY^1W<2p(#=aIOo%1% z1--o=wOZ_IUuyVH*^MLFJC@b<98I^JaHrRnV~Z2~)8&o4JA2)uPqNC3p72>Y9M$Wj zb5j6F_6}p$A9P)p2H$Al{SA zOB&A1PF8jOimgyL#;g?tDY%<&6%z3P`iPHVKPFm@@oTy?(&7M#-jAvN2Prx6RuR5xk8n{MIDZT&8_#^+G_wrav4xJ;!$*J=2kTZ#Y7Q_i-+@9NesUV1i@AYX?~ zW+>TY759peolf)w-%Bpb;ONf3mBXSa)X)a)cv`#c8$v59x?ixv#HO)oTF!#D?FdZk z?SL@Qh4|aQ4i;+#`@ZB)mWL)j>fm5p6??>H4tqcC&*l!*D;_j=QsOJY@PmzXRK8qh zGoPo7uYS^C!>UCSQ$X6aJ;-$0cT)2W{V=|c2UY2WR6Kg(BgyB>i#{I6;?swRWkD8q zi~SHKZONZMXSb}yJ0Q>r7)NxO6cWqfLcjb~Vv7vmF||!>X?($#o6ISIMP2i-)+wI9 z7_A&iF=?YHX(HDd+`S&N30|Fh=KQ@kz1OZ;jfOaONByqwmx+xDh_0(PczhxZ(T_gd zb40s1H5Y+)z5Uu?^L`i@4+^*Oe3DhXc^nZwZHou5LSH$hZC8R=&MD8H#gb@vk40d6 zy(Sp;KA|WOHNun;jU%Gp(?&Np#M@Ixd_8~ykI@Z>nSMvga>yBmI`LT5I(_YNST}|9 zKSJ>@N`v5@=YDTm06g~oW0%~2n54Gd_To*9eZFt=;J1)0q@#|%n8(R-@wF%~6XpHm zpIGQaIkd|!-;s?SsTR1WTsdOoRksS%iKV~Pi;uY`9r)2ra_SqA;GcdBPKP2R`Wj&A z@F_$S*cr&OGZiD8k1qDXee0e0k`G|%=AC#D!4*zM&_H`{AMU#{O7Tg=*ckKSGG^i` z5a^ExPvhpC<2A%)*6~i4|Nb#@v8I`3yLXo3wAFXFwM%xUxx4#h281ZxM$N)&kOx!j z+Xo?aG$$&R819xMzM?ix>eIYY%peRzP8=6`Jkaa;?bkava>B6a!i0Ry^Z z8>djmSp4(}#sl4*!lgCf_m!$K@IpnTrKN9>_UpDn-3BK1v~_JLuGUGD0}D{5M~vn} zJxgt1f&>b_u|+x``%fXAKb|w_HoOrCjTQ-kE%>oba&Qhct53x8JY=B1OW>vDE*>+X zx^`lCM5D_u3<4tof=Oy$b>fr_Jl13AK48apuSlf1vko=RdCJ1ll$w3C-_sU+ah09x zhq+7&3Ei(>4|Z3y1WC*P)Tu3+(k2ht;5=*Y~`E{jw>QLELIZY2)qYWpo5 zj^5IA{h9_!-3+RcqMe{YT_sSwx}<@PiP+ob&fe#=+v*h3gZKTD&$!294}Tde{nM|d zR-yL##-xsBTt@Hg<dw%;CBfQ z)nZdXyb-;pxN*kFpKezhJ;H3kiuKe3N0vTKOw zW8L0a*ZP5uyxg7e@}h*<{abnXx;A6`qND6>(G)}nm;1AC7h@N*8|gOhBi=eKal|p2 zGY%Gdr0uuOyEMDcEN^6u1~_dN-iT3AwGS?0fruT$*R1!{PV;8@nyTwb{xY+zjn$+! zI>x0H@FZO|d>qG+0O~%_EK4qVu0aX=0KhIi3t)#C3Gqfk&OKe%!-Ki2{VugBHO z)?PQ8&FWTHnlD2Lp&hC`9&HMl)EIq#Ry2aJqZfq(A*9)fqAk+V+fHVe#=i3}1k6t? zG;dQME=oyY4@DWyle8me&o9_G++c`!#f)t1z<EU2-BU#a_IZ0e<= zeQoyw?fj0Tyz>riT=&-GQYZ$aZ-_946see1Jeft*x+e?avkRa>n+rO9m-f#CpfQJjo0ZW#?#BNIh4Mh}hzGofj|&TBYe)TMvW{ z_gS+AU2}di8~ckGskMu=FW)CmGBA0U`ccy9WM6EXw4~)bJ?l~LOrwti4jvHB?{ZVz}ctycJzd$mP)UckRkPR7doK-i9ZWzDJ) z>OdRQW1D*(gXI9q-NbUe2MFU9$O`BPt>67)1B_ZwX&*;OO{hp8Fw_l8jUpeD`D_9Y zpSeg0$A&XkMU*m_ROaI|^9tU~7gbLmbb{~Q@O(Llx6y!&UAutxX>^h>YPZ1icf^?u zoX8NJ^o$42%TOKcdUP2m%HD9zsRbqV?QI??%&)`Y>sa&}LBbE2Yo?R)GQ3Ni_79xq z?I7o@xj%rK6cwAve^S?m?$dy=jihdI8C!|tjclm(^5e?u-HMP01!k1Lp+RU=w>vc` zsN2z>>3*EQYjBQxZ0=|6_fjOmnkI?j^N-_Vb7PIeAw_3%XulPBUrRHdDrkGN7KHts z0X7EUQTd9pMp4Dqh0C{*9fz^-0IGn zjbZx?qS|_+khazzJBcTaTDe6(KM9M=G(-m6vJ*Jf<3dOw?sE(!_l$`A+)RR?H-+1u zgrh9*m(`D`w#UNuIAb(K`{gdisto7JuL5)i-k^)4Goo>3O7GVz1WU7qpo&B|oSpr{ zds5T@%I6<&PeOmmpKwoT@^A-ASTMSN!G7hlv&!&;e6Ic=>B%pwqi=rAIy{Kp*Gt0F z_(-_-pl~?!z5UMx_U@%JaKz_`$P1QSIZloHE zg)&xbe0Q79!g*BAjv8vIl~ZpCaZFv6HkaA%)du%_`7+&dm?+Fe1eN8qd58C8>eBX# zw8#MiM>tfR_8mYmezbJhCkvWjb2BF#w6$I@aZQfhaRF!r8ZFO9(lF3hYWJY@Q~gy!@C3u->x-P|^Bqup9bE zRON~10WdnY%^dFGQ{PEK=sU4m;LwEcy#>F>vw1+(y!e*nYnmR675P@~2D5b_b-942DAa>uc00%~$FjSTe!c@w)EfW+(jhan7TqFDc(Ae=kqa6X1HpEUef z6iwSYdI2IoMg-DG+8dKp6XKB{a59~X{sSDlLSXU;sLCV&a)4|vnP`dU~!(d5)TJ?w_~Moy6A89f#UQgyx`f9|YAXKy=y zukr&1c#VQLYHUHD!un>9aNgHk_R=$tlZ3q|pezT&Zt2I~J7{QH({ju!IUy_+@c(=V7G@dalfFI6t2`^}S8iu!cC$6&r$`vjVm3 z{9}_Vqrva7x9%yi3|+g%z$6u(Bvb3O=~}$fFoG(xo=3nzpO(GB_X=jOwnB9G-QKAS zR<1qy^|TMw-@p7w%5ToB{~qse6O_d+%nNW; zW|4?&^(O+IoXULbl+1J-)WYPe|Ws(?YdN5Ksr!cwSrEHe|zc%lbkR_us-3m zUmQ~TS|HCma=-Rn8z&n~Uv}K1Xere1lkMbFg|ZQZK#2urH;tB%F&+gnL7>+6VyFH6(K8O`1f;}HvHOfMYeW*wB{H=hX5iD}2NJH}RIsLt+=NE~ zS=W)Rpf;Y>u*|J}Req$qnZ{?~p`K#%Keqx6=zWwjYIE8M&@X_TR{`%qYGwCM2)E9$(0jsEp2P^8w}FgPbGY~b$ubuLL+=tnN3t8$Lm&6Oo^1N?EXzmx$6}9 z7bz38IrW1y=V7qxD;R}Ts1ifkv(Gvx2|-1upn6uZ(UbMTcjKp1Mg-!MHf0Be27kSR4&tGx721*|d5D17~Shnfum1kEY-`zME&QiYtBrw;H5X^n^iWE2Aa&EPjp8W>iK!x?5C z!Dybx<67_O|2)0JGUKH(e}S>}t&hqe+0u@grYT?hPe3$&Df&^mv+O5SSr4Sme z?R9Cg4TA{1`tR+Pbq(-Kjk#y}9>3Hd_L)mofruB}5|9^Hn2L=O(o}e_&=-(crf7<4 z#ddIOu#LD;_EcbF1jo4Uv$!dRO)i4Yr6;hbrDt*bBglz-hvM3U^qq@H*^V0g&Ur!}7WEZW|@;s+z_ zv>zg$PnUZxi-{}MfdiwyZ+IlD)Z|&<)Z;$VKlt17l;AzF?fuzE(b>Z)=Q(_ z9!dQ%h6O3}zYABLaafDv8tPr9729xAZ={`4Zo$AfNLZ(EeM3aT#sDT6jLDT!+BBOO z@Dwpls{XE7I{7lZD#SGHL?`&>`qjJwiM!~o&Nj`qr^pid@&naJ4UH=Ax=7jY+4|&P z_?)<|UGCLabWnV?L(DorgH+>_r-nTrvAq!2*m4djQJ5~6Ql7%MKU$m6k9T}yO>fi- z|7f3w5H)g6Df{y0|73rPz5H|44DZCTau>Hp;&ZIK7(Zulne~e#aa9>}UrchYz?Yvi zIv;YUd{5U7}xD-Qx)$a5OCC4TS)xX7giDh`{M{OW>D z!pw!k{_E{GM3@OviUSQ{1OQIfYv9m&w}nzR(i%ny)5ud08S8>~9WPV2~;{HiQT2svfmQ6P!brCevigV)e@Jo`xVdrns!3Lj#w<56dBWqh(tXlDXQahK;icoZHfyJHrmF~R zJ@YlIZiv#m0Rim7cjk=8@dALG(;dFj$zqd^n24EBAmAp>ASO^LJ05TY;yX6>Isg}) z4PE1}qgMwO)aa?~EtgIb&*cq_l6S>lgRK7rs#p1A$5i0&{vkfTMS22~5?yr287o&%YV&#GYWAv6TM)Rp(`BHz;D9K zn0C9u>l`7DW!vk@C6x!&naN0_mjb_da$G)=HyrTt5N>*&Pok>Q#hBeEU_Q;|;fsI|B>D+R7ET-H; zMP@*#QE0OOh{h_&Suc zkvZs?{$8A7FY#;Ac&GGO$azv|gg0rk$a*o(mP{F2cx_ZSg?6;gA3~cs;F4U=TJGl{ zsVK~|4`c`J$%86-Pb|EOUaCju2J4r7C~&$L+SO-=>`_g1JJ`#D3@mwr16eGnA#|b} zB-#V!7h?8E%WZQoFII$ehXsy_X9tMqNRcR`ip|{a0UvST>|_e_fNY>xpp-$+aKK&q zsg$vY;URI&wdy>Z4rW}mVxuY7?cKFSkT+9rY0M~4MC3%rPbfl!g`@t$Ek@8LC9~r?otsXp4H`q#!Cq z2sdzZsiqj$x-^KP9F`f-Me9B8Q%aNMz_V6wy^}62Ju?Pvm{>uffUekgfXen)v5hcL z@v(A&ptczyx)s7imgJm~*95Rt;bb%CSSy019yIQ%>(^NYDM?Ey6J|Ik|SSbBAaMU|fN5t8va-=&2A%?qs z3LQwV)W(e=Rt|w05p?1Wyal*(F~s?(5}1!Nd=9dyfceP9s)43dA5{S-t{G<HX%{t-FVvkRxz95+<_8Z*qz}l7!2ZfFG?)pjn^;{vC(oi?b#ke$%bfKxiEC(QBI~ zsU=*c*t~kj)93c#al4_H|F&f5;`I_*crp!mveP5Jh)~e1&}kZQcc{?^X54ci4mYmp z+&AQfF)O<*f00W2a*Kh?)o*54$hVK_8ah?TiO1CA&V--1fd`p1zBKMk0ALvfHa*pBe`tt>PJ@* z)&+`;`TLkm5)-~6&vaGkVgZh@!Q{KX1(dr1!!;>4SbA?9q+~SWb-!sfHGkeW_o#*-C5p>al9VMSs##a@;D3ci48|JNbD=HtWyjSy?;}B{m;f&Hw6SL-Q z4d`-hoh^-Sn#0wjL^dh=sLS7KjAjRl`BR9dzpN)hY`59Cc_mx48?CtMa*zk8r*164 zujLBO#+5eK?FK9D{G#wTlq3*28O)UW(bO}S5d_ahe`$Pi*M%}NS88T{&WRh!UZQMRsP@}d={Kz@&~{*G|{#? zf|QCBWyypTPL6RJIa^rsk)>p68~r;6pX#q1eBQ;Jc^*db%carBTVd{ODR{4Ae)G?S z9#^ow!dQkzJr|ETWwcgX6p`;MH5E;7d<=D73czL_<*%4?LsZc|gc=b#E_ zM_(;tdM6~i$iCdw#XqtAtM0a2X<&nkI&uPO0GCWdoF_3;|3?GYTt7|s*DVk6RS}_E{EMsQH8C_QBf>uy1+ThK2)OSuBhIJB4kB zJ%@t>qLs{0Vr#*cTkvDyAB`!v^rn&RH9L#@xj(p_NX4mpj4Nom*fEyz;c)koOz{(o z^ne#)7)#W=(pU*u7Q$~LtLdb3t3m9^{DC%?)o5ljPdtQ-o>o(AVxu>h^L5DZ87X5; zX%^>souPhvu0Sa;r8G#>)5P49yQOCKdIw}lw4E-F@W!;Ii*mC+Pm3RwO{V@f zRk)v`6W4r7EGijLQPnhMs@`3&W7DB=^M|GFggiRI9Il;cn)DJ7*Cfi(ff0ZC&a$YU zw~)15S(-ReZ8`jy`|?d-@Q3%JDLXAb8}Gf_6v(UyX+Tw%UmdpXk~dt>J#0if=x7SL z8s&__os7L70MdQ_1AcmjL97o)Ar>++NNo}KTUHoh`ComemOlDDtkF%%#sai?N9rJ_ z3631jhc>1}5y2A!<%(|u@!GUF!!)8FM8oqbDMHp>Ls%bG*xTs#`&&4T)K;U(eqA64 zW3kuS63luI<_eIs%HprJ9Zv=_evxm8v%72?~KlA18^ZtI@`?Tu+!B7MzwD`a;CS$njZ1vQjJthIc3pVl;Lv% zB_ZN39+HMvnZEz-LZvd*jCa*l$17@}iy3q+4O+Sx%VnyY1yASPCr!$KKqC5#eUjUG z!olp2f8&FGSbwl`0ZB9BC|J;5K#S!!39ptOJO zfe}Y@dP*JpZH*{~c#?KIUxU{n4>62T+F#BcA0GQ>Tg9wcJGNr7gA64oLD>r)TvJlm zDOe+fl>B}1NdDgRZ-Q_KU$KT4;wJ)z787zjxmjq4%;77EHWXRmy2Z^ispQG-wcc6I znIUe!XVX{eG0u&*OIj`uc2b+iEsPy}(2u~)l4m?t&4%NNdvs&5*h)9Ia0CFAQ}GO@ z^;gFUkKc0F<#4SO_Jk0|YVYj9xhlu2)3ww}-y!F^upGsPC@V&2FKqm}37gV5OA&D7=Yj!+dn1d(xWfrfSDF zY^*=0=}=@uufybsep6XWFJ#akM}PF!OgGH2)hvDV!#yvhUrNuyYTT~YpP*pQoP_k7 zzn_<)GwW>mi`3*un?CwpXu5;{RzpH|g9;zjvsYH%w2HCE2Tv5$D*$f2#tlwqn_85d zsNn40iy_2XIQWRk6luyo|D$#A8-p`;J7BXIbS`r<5YikFikM?E$&YHIAsskLM5?FT2oG^!c(vMa4?@XPmv!kb5OyJ{@kLsi$5OztED33;7@HYbE9{=^p4c+=9`{F} zvIU`u!$eEuo)7>S`<&24B!y{|n@@DSQINj!xer;R$dyBDVsa5W?)~oSoJmuDhbkp_ z+UpiS+#*w3!7-WjzYj;_bI-(!U^q;7bPNk zqAq3F{;(1E^38z`u&l}+_$%}v_r)C#+Mi{}i$7lxo+r`o%ieyM;fc?6#`$nTI!JzQ z&qWU4_Wr3n|K6;)^DjA0^PZr4&)(6SmH3V2c}f)PeZ-j8>#gzYxva}o5&HGDn<@Ve z&pBOqEYh9g*M+CY-80g+clf%3_HMSXYKK7~d#&~W(zm`I7uIaaa zCl|kfN_w@@3(ji~L+X0UCxDfETW}y{e49XHSpS z8((>q49Zd-CF9sG{D#J%XHR3N2-rSs5?z}X^k1)7$|9}^ahcVA0GWh88-9I z$RPAsrN7YK_U06*lCOL7KCc!T#DBrhkMJfSRxNg@g@1kJeO>0(qBOqF@$P586aVYj zXhc(zsnB39v894*R)(J9tF$!T_2Fw* z-z~N|cH7k)Ia{9UZdOEBjTSU<7Eb-^dbb&cZv})J@5LuBK;CrIhP{ba!p+rRIqbbI z4jJgKCHi-A`o%wz)3E^39E8{;`aIsPh61-si{QVZ=5pM}y~_K0zYy4%KO<-LvHvcA zusT&MLIkf?sFIU5r|nz=7DFx5YZsMvNtSI2lJL>XP7TkvYY>58>aL4tqH|KM?maPv zdUhTsRHTQB3+CjS)o`a5vo$=JDw^~-_~McS9neKa@a~GDRk_N2+Ap87txTwf_4cV! zxjT>Jr0cbRN)Cx%y32X+%?RaA-%(82xlxz*L*$3!cyj7}{{WGW5qT*`pe!1}Vfk3V ztNLlBiJT7n)6)(_PW1w15j_@V+KA7_D4ldp*c8S52n|HCTr!xoW0gLjw%}~!X(Kq!^Ki_$%l0NL;QF14 zfiyhBywPJex`vT(-QV%&lQYgAIjlRfU(&o_<-F^C5YUdExH#7-96!E@So*C&Q1GI% zhIXY3wiOAbXky8h%6&nd+=kXChRK%vbuqB#Tqh^M! zo38cC+a&!Z4I5K$!=G;#+oP*AjT#ZW=*2E&Z1%REt6t z;jd#VU?wkxI$VU+dPzJ%dSO_nu}|9XbgU<$vZXEsO|M~P_i=5%J~I>DK32DzNIND6(T2;k$V(@S3UX1PE( ze^;OPno#~W(Q3QvG(o4^5g5yhR*iWS;M58R(;59OxBjb3M=%rbk|;^nBq(gh9t25Z$0YEzA};Gfr_V@Ye+OPiNDgpamJe)71~$jgQLYs@$lAuVoC zT-V*}&1cx$4?jrSNO9}6XyA~tZmCnkUV-wx+rKE2;4BqIiLVb0ib; z?b+nz(dp-J9Nol0@!^h@`=35mu09=#Bjtd}gvF8kJ4mEmv$OY zlTjDRkIkGdw_x6DsgOu2A+nxFh5h)p1|Cgwu{n%u&|tcMF|Qu~zd|A=KFoOv)_1Z3 zaUAiN%q+WkT~p6w_3b*G)LrxBx0qQX+sNY)PoTCo?wSRptRN-wS%02U*I9+)LNE@( zX(GVZr+74+>L#kb`5@o;s)#d_V>9&e+@)0{H$&Ca$`gUn2=07eFBx$utcY#)7Jf+K zJhBUTOZ&T4Xs>wM+X%LIW#?D`U+Yn$Rym*W$BSbig@G(3vNmuwY2rAWT9r6;!I^_*}VXh z^fm!bsVS)?AKcHjdX(Kyqlrj+N|*CedVTY>ZCb~nsUJ#w4mNjK^@no?+wSL?i}^`< zev6+QYL=SvR)9qnpxwO0FdG<5F3fK-pDdf5Xz`o7hwYp=IFL~hq~0?)L47m_v&a?K zuzA=jLZ`YcJ1}Ξ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 IMfH9E2NA`3q5uE@ diff --git a/docs/src/archive/images/install-git-1.png b/docs/src/archive/images/install-git-1.png deleted file mode 100644 index 7503dbb61cd417689b0874349b9f4acb425425b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17942 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 VGC1_c;8Y%fh=AnJaz1U}{{t$O#6182 diff --git a/docs/src/archive/images/install-graphviz-1.png b/docs/src/archive/images/install-graphviz-1.png deleted file mode 100644 index dc79e58f1a6be18982033a5298c1897f4a352fee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15790 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 diff --git a/docs/src/archive/images/install-graphviz-2a.png b/docs/src/archive/images/install-graphviz-2a.png deleted file mode 100644 index 394598db7231941cff72bef4abc50674b375200f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18167 zcmdVC2T+sW*Dk6e3W5|-klq9o>AfQ;0@4vhx(I|Ckd_FcC?Y6G6QvjF9fUyWp!AZ^ zTL?vZOM(a_Kq%+s_rKqF|98$kGvB>)X1;rwVIbj6*n7Y0dDgR@wRb)l>T6!V#&Ye# zg$viUwH_H?xNvb7_ztDG1Uysr#M%M)anajYQ~g5u0NV!e;IgBdp4x>Al`&Muw&cKL z%GX+E-WM*=w4Z-p?Di8^>-G*Z?AC0$n;kSU^Kk`<2iT@ZPX z#uCS}4Zq=%5}VH@u;==U4}FO~P+z^LiFm>Ozs4ai_};YMGqw_WPn#e|Yx1^da`6z^m;*eY}lI2ik8y{tW-u z^u_ttkx3fbfI*#NgGx$~J9O`QN;;kT(G`?F1$Us`HIQn+!RtbLbqZ!%ep%95X7DLe za`1Tb-PK8zs>Ai+oUCtj>OF-cE`&sD|BaUMl3<_1Tm3vL5x0xD%pk%i&x%V(xS(AF7{eeR%LxcJ6G*wZI_Q^JtH3DJHbeokpnf zcUlj5KTYUFkwnYO*kA58@u9Zac)Q-U*JO;J!=oNKWK4}Ep{Z*JLk09xaoI@)RTT-% zwg$nbJB3{zMI7z z;n!2H|7Z-}uQ<~~jJhoSUIw*pw~)%~PU~A+mWwi#eOz|Hu4lyBQ`>QZSQc>Z7zt2tDU=+J~B3|k*xc9GF=%}f`jsuZ?|@bpRS4b=H{Q$VK>A8S+#9| z8a#(xredrUqHms}2z^oU$usd$$lcFX zmGZFHItShR_r@Q-j?=grmUTVmy52YsLsqSLp?J28lB{=xbLQ0!Fj4fctATg6miO{^ zU`!XJ7!`k+R6&`xFI{q%l99IDYWdQdANu)Pqh>Gmg5s@IS3V!z$QkVKFP)Prb;PBb z!5077Vp7m)58`UZv^TEdYDGPCf4W(;`EMuFeSCvt$20j@laS*2Lf0y%7hI4=TaBC& z|BG2SG;Wq0OzOMwq}Y3Ays2CLp3yCD-pNiORBQYb%`_bj{-3lyj|9dY4ZDZ+SCRnpwk)F0x~cQq&0 zuGKyKGu!gzwchQ*r>0woN|@|2mgo3p4P6K*bJj5x%^7sC;HyYfs@wiKDC^z`-Rgi= zSR-7P@-cX`n&k-#j}|DZ--G@5DELJ5Y(w>wv>gOnpdt}Ih=1zZw`?6q=UORmSF2f2 zuS>2x)qHDFS%J?6#XNzhqnB2zuS0aHMb?Tq1$)$Y(aQY!X$3Q;(;ptYMjA4Y`dO=k z#}2CGx^VY74DB3SG+0L{lO9NR>@i62U^)2Ub}4rS7@JC((_CWILOK8oxgCsHv3d{c%{5Spv^Q(m+fce z^=a#d0iD9Ak(`Uu&N02RVO*3$^mwo}nlGhq@@uS-EwLwwz45-XHJB}}0lj_PDO%AA zLqUW5m%2_oYyxm7RvZ75sL_H+{8ZqQD86O$C>7sAY^i8D5)7re>UhzWOX6(7^~qxv zE%lxZHgOdJX7~?u; zfF5PC;}>C`@HfQ}UA-qu>rj z-Zwwb9rhXOa0Yd)`R`~-?p%){Y$)A`tP=#tGOWL_XK(gfJWlNARZsTFzkZnf-gJP= z6Agl*Bj+0ldBH9Cyx{uNmF@XIe`(#{94@ljv@GYF8V33FSBN&ROeW5+_QfQ&HXKBs zn3uQgUg#V7R)O|fYW8q4Yaz%YW)2Mn@5?qtc@1_!4JVXX`lh?*G|XCYVtwrfP2p7w zy}Wl>p=^FWlSW^K4O(0GnN2z71F@f!yZJK|3Cp|PP^@x`>-fq(42n$8~|{LGqffvne7wC-cxZwG|@O@*q;`y7nc z&Uvq-=VWHFsSplf+rdG52W#7z2bWf+uG+Av9(I1BH3?E-~VB>V8|JxryAk&Ibg3eZNI6}TDR*QgYy_n#U zGm>zN`be~7Jv51(PqMMk>LjDr3TfSEg%t4$M+395MYXKm5+JVG_<98|{M38}iEB(^ zCkHXrw;m1@w8|eB^_<{%2e8{MXUxdcxg9RxeKb%aUYhUF!K96C33#xlVA2V+mFRW$ z_x{ZSXmL871XV{`VDB7g+B)TgFBK7q*1*4ld?V$8wi93_^R-R2gi7AKGTx%25NTKf zq8)2CecI;A*|K*8WZQ*nNcU05%_OU3cT~{9l%orQXfQEKh*3RZN0tNgQLOJyo9(so zh5#x=f+{zS9b^j34i|z{wK=QG$BmOG-^;^2tIs(OI2kUTXuSIVg*s#wS>M{%LC3tzk-+L$PGyGx@uNk`QwABPS>enm!q zp=5VJHQ??)2S2R$J0vrW+5qJecW;(IY@hJ$4`iEP_V^|rmKxiyCuw=vm>`I8P1ndbWl05kr zD*ErJO;c31et6M>>kA98^)==Di_qNNO{f4-KxGJNHLv*rZ8vRg<5KI~DBYNk15-)F z!d}G6UxYWlLgAO^EL{?-tx(a(+2q-oQ!NVahj&r}cdYd=8e8J$b9dQ}?Oj&+h<>*9 zr9d|AG|E-htEfDsS)=S8niRac#|c^eN3X9DlG#R1uD-4eI7! zZUN0gP&3`lGn}xa(DGiNd2U`=L_aPD?A(}SH3C`2a)UjDdg{_<`>dX0N}L-9(X6$q z4c&jd0BF+{E@M&T%st9FnD;ZjHSGVu+tG`utp9JM_mONlDvc(v!tXbkFfI7l@P3Ta zo8tu%t?KLfERtz6O=tV|?d*F>GI6K#C;gs{LOHGy1cIixVniV4e&-uI*d(bCzip)2 zn=m~Zg|o&BsJIFBynrV`)o9GYS2ceNE@} z7~q^n4VW~Tsa_La(5m?i4htFm3OBm~?x$)!9?*W$_(GaX#)=5+O0J z)yA{m?zIB{qv>d#`;z#IXHheJU{ipw`pJ`DlXA}D^N;{Dh=7Px5(_jy-0s8|V2vAl ztQhS(usl#W%TmPLsoX12rc{6gJSK8n^o0yfCg<%$k1qEUe?Ga&`7uCA@n1`i&(@#5{3v z!ZF}f1SGmr?kZ8m^`1uuw-WL>9(Mi?j#LsNU}A}QRCV(Uu;w%koVJR~Zqoh>+EEYx zbZ;ehfyFRNiHpCs?8f9G;T{J&xBUe{d6+GsdaQgqXEWq;kFrIqxEIu1vyFxv^UT*N z@C4Cr0m*n9kjK>@7FRZrwVkIr7ojO<#Q<=zp!ITj5L5k<>cy(x(T94|b9I&M8cgQ>+l3aj(OgcHUK0M|E zSl4yB9OPjZ>q)6t9W;&`{NQ$y%~wU?eU7HxPmTsp=;5jdb)@@uh8yl+#S}RfJckuH z^4w5o`B+#Cy#ugrQrfIYPmeK-7=C;-6fSjq-)QQ!?FU;{hQ~fyQ&(2VHRvy}_PRB$lVw`1Qt;%&GCZna(E=uy52=jZjv;{gL|rc2fAOLMJo zkI5*293V?U?C~yyBm`4%jmZpmyl~IQiM6|qOjg;_BCn;89O=}-`Kgv;KcXlZxFZQA zBA^$R-!m6nbf>IHJ8R(&d(5=NNb{$1%h;;1a%b2YsW1w|>8o}{pQ()3_6(l~eXySG zYmYzAHLp*Oz~Ow3;TF=4^L0x-fTa8|0qC;p=Ha8=vQ3{ZmGR1Iz8lK&)gIT_*>X2u z`AcN)jz!F$^{D0IRO?+6Z|36erpj!OR$Jl06(Th`CXDvS^Er@IAUS?hHH&bDz2THj z4*WtiYUj);o(b2NdNuxepmr{sgZSLxhs($6=8Rqx%}X;$p{8->#an(hWB396B+i7L zmYkD=Kv`}_*W@ft9DKfUN6*Z0zH(ByN~eux zHs{1~%lgCj?S?f!EwfM+@k*Oenol{=jBbnc<~GMD&IQlz@*EckFlsSaGNPND85&N# zO!JS>j6-at77_wYlB`xTgn__c1L(U8a#I{&Zf}ARn3hU{V2}W^(*WIZI@5bV!7Nyh z1^|B)&!>&ya~wokTgB4DF`-r{5!tQ>^IC(moD7GJ97gXZfEc|DSN#*aTL)TkA=p^^ zF)T+bm_#c~+k%tWV{Fwx?WqTWv4Gwpr3i0tkO~csXesWiRsxW3di|SM4`WMsGR@%; z?52b1j|uXrRh3DcE}L1I>(<39e*g(AQ^z4is3eB5Bo{9zC-10btTqTxxJDh=$Zfeo z%0_8Y2xs}jX`1@>CAy<9fmxI=o$?o-dt!A$^-kmzC~8cBC?1rT#VTO{>=VfbBqG4 z1ki@?@Rx*Eage4F90=qW^^PUDp(DQ9hP>N)Q|ahd&$6W1z?7xMG9IXv*Si^`@LwzR z!r5=8*aU6PdfZL8eC*eqXdOngP%+KCvT70Nxa~w|#jqLTdhZXeB@egonSBOKf3=m$ zBoiQTQsG^9wXRY?U%S_w5vp}Mg&W|Ta_rFwgW;6HVJ_9~ksv%KLoP(~8&FL#fOK#{YF)E;MhPm# zGgBgrS5z_{_(o?Tsh{6JVX_jX%1viikCpBZafeIEsOGvTy`Ubzg>ihJOby; z!B(Wlv?%E8^qH#;T%ZgFxADS^^6UWl=ix6p@l90=V)4!T$YGaeDaAAqph9y9*s{P^ z7*}|Kx|fN{{x?90v=s4qkW7RrGI|lo@y~LCH0c4*!rKjN;@Ww zLz0x^0lQZ>MjH^}?x^TXa3(JRym@HJGS@Z{5FmR}+uKSwl-_ENH!#}IuNZ&6)gU*^ zm=xIGF-+o=cS5;uyO_fJQ$7pTI6`}>g?qRhYk^R1$i;~%ju5=m_zf@?s?J>bIGcP2 zW_5~Wj$hdMx|y=BIuw*fRQ;zg82tT{K9BDO-1zqfN5STS)3SeXtWG0lPhPL_v* z9})}8*j_;1k=_r?(VyY>k+T}2gUd52nSp`Qy*~@lneo-l1eMKYDv5?^YjIfQMDXh+ zoyhGe+Q5^;~;ZFj|{@)Gh=suYrTVr zb}EhY#u$y~?3zReyoI~U&)4(+ON^)ulQSnZ^MYeQ9?Mvratk|;PxoqCd{$h=&WpR8 z2a9I(1?|IIwd`RiBZP{lZUkuEoNG@Y^XMhO?uEQJ$ONPt&EkvZr>ui^efhvKIyfB7^^#35=MR~}O#AD#WHUQ0AGQJ?)bB?# zMg^5u6FwNsCSQ}Bg0-Ez=rE6@wdxKbZxDqpgYiP3cGZ!^$DJ+F%zIo_NqtHyaZB?9 znW39Ov%d%6TXNI#nyAxbo9=3$>`z44)C~4D!_-zuU0BnZzez62cd$lViKoX7!$-=j z^Km>fHoNPJcL!FD5$r(+pNKLlelvkIzdDRklM=}RB5LcZZQ%n=!tBPd?imHMlh(9f z5RKBw+D!Jxq(VUA_sv!yWKZq2&lLm;;GO)Y6Hl2!OX30dIIaFzB-inBepnTsbkEmA z1UGA>9(2}T&sNgP=$p}@XNoN(6YoOvz1f!|xqXRMqc6SD$t~|Xx;eQ+1)*BIVk?Va zcfKM!O}!siUVEsU0Q0eH%Qz9)gVrbO#s~|9f^sR{9F-?{=udCEy&qHay&^T8qa%*n zl~W&m;OFc8W~y&h2x^4*d?hMPS*XX}%i~<(h!T<5QyS)A7&Y>AX{DlV1Fi7c>7)xb zPghof{+lty=76)?ZZL@1Ay8#vRPNCM>T>|@IbPaMR zzq(`ER(occW^0K8taCqBM#3$5M_$*#CgGgU?Ezl@t2hJ)RnK0=Q*kE2o13?3yZ#sVW>C=g=^p!KpH%BLBgVp&sh}sqBMt~u@Nriv|wh(Bt zd&^%OJctsp>E<>E*dY;CcS7G;AIONbu7Cc2EEKIn)0&1r@SAif~RXaIj(THpTU_^d}JxR~cOiYrbmyk}%lx z+yTtQ3`prVe&LPEkE>}!&;6U^iB9aI&+t(~x_~{m!+tdFb9dEGf8oLcS^HkfO5Vw^m8K*RBZ?}MwE>*B&b1q+8ZauVh(qkd7h za_8KDk?~;6FVhcWL-H{<1az$``R2(_!EBD}-j4L)%P}haU0am0AiAO0+%biRh55=y z31XhG;}CMsUU|gQ{GT!VI-2y(r`Mx)FJtqgL_zIwPi$0NO{^Jfa+E0%_%wj1^a5WfC+jAH>S)JXk z5e5t*3t*MTfm(#D09H$TM#D^eU-nyI=Jc(LGI^&fm&Y$Y28tY9(~2~m!rqASh%~<> zr1kx@(!k>FuR?N=NDa_LT=IYHx)SlJCc0*h+n-nUO=)-9s0q zGxAmb5A^IU$R~hd+BJ9pdPi92f7y^Q;QwcbNm;bF>(CEbopH^1y@_iUd|Q zO$^%iGs{ImKo!d}*^kKDv;!tnE7wz5pp5+TL)2!gIzzZqt4&0fH(TixDTVsyk$d8+ z4r~g15}9#oUvzE~d&TPlHW*-+Wb`&K(J))NB&Jy{<2~FwglYyyQjlzR<;ifCH_l=iv*#oxLKR}&LVgK@B29RL%ttDs&`o~C`GN3l|j2wNo^*(8K?p6+E zaE#YsVNiQ0(nwji7wgZ>2ggEVQj%m1I zY%Z5943OOcBM;2uKY^eg+1WkDjd~bS&-qqQ!x2bN;6~qMDcY({gjh_Sy^{5#CK;P1}xDIEJ9Yb5^&U|!9dZQkDE9TH~vv#9z?wd zYbXnYknFy|b^}eM6#-kxMRORKL83=v7?>Mc8|LmF6IVT34v+B&bR70?h{=1jvXL18>#*X5f}6s`;!QL(U8|Spa*= zYMH~EU=LhA3jF7{m*F;hIm!)OODh?3nl&ETeJP(sY5 zNzulz7hq!Wmk2L3xFXSOP=bghbl-RPqed$PL>c|5%isJMTDn5Up5^P;f0I0@Tl8&a zg{n(6hl+zPGDEoMt8?RNud&5+Mlw5}1z6SRfFY?l4nfL;z^7HT0Y^K=8KygxK51}X zU0!rv#%fH@y@(f=?ac+!g$vLT3qA|;iT^BA*Iz|#N!|KzCK8V+=~WcMCd{62im1)s zeJz_t1LggY$msKH_+tm%A;m?CK!3-2HoXLbZw&aLubZ@MXI4 zjY&Y40?^JW2xx1JOX9TN0KUbcckYy;fKw&mltcR52Q5Jxl>)WU>xP@d3P3}cIabNh zw6z~6PM}GCrIsIum{f88nR*iT2Q|q9?VWLOc>D$W?D+b9V$WnI4_{){NHEt$x9G?8 z4MIhcqvD3f`+BKFv$B4VkAEfSuUf=vdt^@o5uoX}Q1eHn*eW*eytajU4Q|JJo`3(8F?m?DMlR+FHG!xi+@FZLiIDr6J*zEJh?p^OM$N?mmi4)KtOi{E%HSCD^WGj% z1ouNddx$wtrlWz3KvM>^5GJrHV5o*Eh8tP{%?ECUKEHUQNDVCkHTkXD%KeKP5Gb1U zV+ZU&r!~jc#i(gh@&3B{g*JEV)35Cc+3{2cfD>XRW` z0jM-k0b{CX`;~=j489;pwZ^k4pLM1WRRp>1y#zrXeaf?^6mY=YKF$=XCr>s8I+rPk zM)Kslzzg-}p6%S#TF&yY7z!Z(=0fBU*sMiHc=jV&W6K3-la>IjWJeIE5;%MYkjAl} zN^tkiJE(xIz-X@@@bx)hw8f-y(&d9Za&Zk~`t=Oz(;I#0X7p%kVaj!uz7$;7_V;TCw4^vcEOWWw5xNYB#^Q8 z0bh|luZcL^+e{daF~V~@hR<{RR6x}!ob%iv`;F)3p+!BQy-@BeBF;e3at+AEH0Lwr zf?7zV>ckCRM5xyS7%?4%hy#lUxafve=RII(a80gF)|x68H91HVD4TO}=Vn5p0ZSo} zY;{|>mgdi?TW@Lus;>1d0-g5}oij5Uq6-4bp;R5*KG6v9g~>sp%PsSX?@j0Z9-MDF zaHcPFWu5_=zX15e6|Z?gZnkp82amkX$KCylnlzg(r;@k}xI(=EE=$q#F+`OJVAg_A zSBL=Aj&FaDb5Ajv5%*% zg?hebax*M*s5(DjJns(`JHslcIoyMFOK?bFlZ7xf2q2(xd4OO_W^f^&&lg7r2r@uD ze1jp;@iPT31ey9opz*(TJ~6Xcwc%Dxuq2Rw&b#!A^{J|6RnCgl(VBoN5{F?(g@2zH zr&vi4|FGOp*8&WyTfiQ^BXu#@A0_V4*h2#`lD0aqL+~94=nA)}{V%q9{7)MS-~U%H z@1jTw9L(Mwu&5SryIIPi*e!`rkg6V09z-VN4tw8z#!?w^Oc~Pn&Yk?;dBS`#%$cy; z@hhWOqtxew?dKNR+2?g7VTNpZwi)^N1}?Xk*L+8Ad(*MM!%v#!!1iBAH%M3<>m{{b za=t=Rk~983t0Qi(-(zR8zpnI+m-9E)e84nmPn(1B>8ufRX3hHQS5U(5d)JNfWd-ei zYD>A@Rm8pI-?b|}?oq%>FDkZGM{zAQRaSQCJCfKOk%p-90)@O$JOyO%&(Bs~pN#2H z_x!Os$Kk1;;RQZyYrji5BG<@T#|ymKSG#-I>q))Eos94WPG`@YS2o?${&Z%#4jF%Y zc0ATfp3YXV&iFytHCZ1#(%hy?-i+=$KrrV^?dlFd`R%@46d3-Ig{^*XtIT~gN^1$@ z-v+%#j3%?SGuuLt935}rlREP%ksOoEaT#Mgm;ttTyNqS}3oVl>aAR8GL!pdU+dsQS zy+|LsS^mV8GgbIFU8Coot6o3sl_%YXQ9s~K2_5<@^$u6sZGMMGX}d^m^Sc5W3vM_M zC{Ee3iX-(~E1i=fcSvLUQwk*Hz2LJ-s-ht9w}vtf_z+LXIW zk&Jp~&E}}UJA}?AdrtB0_sWAEpY>yc(2)Jo@S@_VTB0WTs}~=px$_XEwTv@dmvh@I z!&eJYv7A=t+ZA&I`BLbwz~@a+H2j#$4w3U#Q#$KMuAEcNaGoFfCzN&uw{KTu~n8|G4d`hxJ7R z!q=<}RZq0eQ~-#tB~8uTEiu;IumpACRSfU;zNp;kEB>Yz>@MlG9)RCKln;*ZI|n-SJP`n_>#0I+TIP{ulOt7;~$yj*)Ju(`kO;o?hae7 zNeZ<@DnvVH(Vutar%UA6JNJB`wQW0rkKxHabC7bw9eIRA-|_xCi(Ul;ytVtJ3uw;H)yS3YIJ{sJ7QFoY@%{KG4R$rJIZwkLBn zNDS7%^|NdwJ=#>axV?$U(_dmPWdB52h}XK?Se4}U(ZkzY)426I$IFbkR}f^h=`R&t zLhrR)iplL_{I~w#{JjmyLA#I8v(P_d*}rO%N7S@(yIgfjWk6(JKX5yCcK<5G7wMGB zA3E-sJX}(`*yy|ziGH0sk~nCtKUF{4r&OM>ec#R1P`)f#GEO={{F@P~xn9t`>bO9Q%<1NaP`_*N_F*z(Z z_n`X@Be+U3+~2P-Zd3Ai{*$%Lt95h7Z!Rfln$cJ3X{HRkyC?ngN-b)XO<93@9J>JJ z_)(Ce%tuemmK#wV!OKTD>9Dc1KWz#NPvDVrC;z*lr_{4nJM5((qf)YU7zRnFma^XO zD)qm;Vi1-*Pvhyy-y&f#?VBQ*`Ac?vfl5PH#Q^{qR4>PgD3=@QpJhqHWKLpvcfjVTM+W9AW!{etQU%D;?A_FAwhMyYTt zBSPLW2~=U1QF!?1)SqDl`gqMay~!uBc3H^IM`Zj^Fv{o^!5i3b z$nD7ZZyVbG%lYSD```aRdwJ3=xPh*&kY(MUn6rbJLDjPWrR?a0Ax&W4Wdd^*ItJKu zi*`t~@)_1(m2s2vTJ9-&TJYzig85JocB+!@{IZGLRcO(cgGD}_gn<2V(}L9RE1M-1 zk3BxJvJ-?9wwp9yZD9p&$?|aVwOnMcJ|#ErPcf8=xcH7~@V@D<73m?@M27)I0dXKL z4}2}!bQ6Q;2(zurujzItyk{HF6@RaLNy_c#fwN@%p}34v`p=ew7G7Np#f<@0R#&VK zVlXcDuvZLqit`AD1q3L)Xu9IhBw~3x+ZNKyf5NB4R&TIxEV>lQ2=ys)7|gCLLW=6{ z7Zy{?eG0qoP{59~4^o8ezwr0B`p=w4#(mj=35+Dd)epKG{O3p#!66 z_HJnKEj#Lsliq6Bp<0yWz>ePG=V>Xc%#HHJ&LcWDnl7Ad=KQ|8LDs!T_E)#NLgW85 z-nPKm5vF?gq}Yp^nH|*G_>rttC(Qi@#WQMY3QA-S4=@Rs&x3DO#fy5}IGBE4Qa;@a z)JxfQoVg-OS zzGcio0QB;+s0GhBc*!~2@%V_lKro%+|ea_t3uQF z;Qu6dwdqG?GGrf>qnjozTztVn!Cj{|tzgZ1%F&w_ulMkx;FqV{457am73nmeBI-~+ zNoH?n*7%m_%lN*gpL6=K9{mB>J6YHd@+p z|Kmi-FND!+tI8sUAJaU(nkWggfbL_7CKrflyZF^gSqv^&Q;JJt;uWUNuvXZY<*2J` z%H{00tTO-i2Hq@e->W|E78HtI`$Xo=TR#f!b}3RvM~4QJ)!s^?&6PM=HAR0`Vpp!~ z$YC2s{r;&!?UN$QxtiYrFYLZ=QIkD<3e(9R=7IO-fImHBa)vo95)LXWxgg-~Ry6Zv zV@(qLa`ip^+bmYf*OA&D)Ty@a#d}T57t3W;Gu{puJr8vojm!7WWy)n%I#ddc%}Qan z3_4sfwF&gaoM6|ldve5QC6|m^{Z@afHs0UJh?N>m60fd%+>GOq@vXT%5@Zj8iLD&V znA0G;9DSAl4wc8hTa|oiAdKUQxyX&o_=uOf{`J$>YMas0_6L05Axc3QyhsTJn!m?F z3_63biXV7L!Q6n3VOPu#eh))70S*VH#Ui5LL8A1Kf2#cfE_lLP1A z&+yG;9c6+)#C|)Y2aeign27_u-B|h1f0Nq)W%Mg3s%8SGdsaiaQ`0jC0ei%IAQq*9 z9CQ@j7E6wT>8~!UB+Sh)%0F9F-WhFLd7M!TC!l%lUjp*_)4d>-#;b|W1Odsozn99b zMuGxfDom2NQj(-VvD$CLuHRM#gCtbD!U`B(kddOrz`R;LXKf{3)RHaA)~lsRT?%J0 zUAV#iE}9)LSavcja98wqHu?3gTgS_LJ-Vy4c4ZM(u$?F9-);pf;@=&@h~52ZE6C7T ze&EiVsBY2sXiWF4%<4>}q!y}A+Bc*-hLRQ;$*@0U&fb!CdAD`n-f z(Q%)^0ZP=Lt_w@_;qQj=8NbzKnO+p4zg~is!b#4jCp!fNd--33kP_Ox-nZRbe>V}k z8_PVR#L-xE(Np3qwCAP4wU2s!V%&i*^}QkWNK4^syfsOYVyw>lTPsyFUD#IOD}Q8oR4PG-`PdO?+;kH zt~@hGXw4eIbr~xzI#f`v2^2Vruh7k}u14nuu_|pSO@E1M6Hom0)3@_E<&T&aw5=?w z)edFvq8fT6z@IXC8^9~W)8ha_*x_r3`bgnpve)}H)puBgz3QhUC&nJ;;=X;tEFJc4 z#7U*|HglnVYp-bwcfw7skd3al873wOP3?ck8U3-!Fmih5z<*_f&pty^7yiB|PzAQ6 z5YhHJF^hNhqY>ivOedRXI;vliCdv+6ZHYxiHW`_+hMsGTM;CJkHS(;M#koS!48!U9 z?!3i|+Qo%sSlS#$5KJT8+jaT%YWI=ZG$PbBo&l)yB$bH{Ie;?;?YR z61OuHCGgA0k3a!T2#w7-J;Z*vxq82=Je2N>xILVTftPfM4HQ@Gus-Cx$Q}9y)CrtF zP+z6{Hzi;vf(jatU;7gmqYS%afAbt2iiNZDO~3lz?$Mr_#M9gMfYxL^fQ`9nlJ;=g zQxt2wqwETM1l)6QF>U55Iu)~7@Z`AUd&^vv{Gov~w_~VnN?=>SZdblu>M3xH<@k7a z5ZD$o(&I5lVN2F!keZB)sa)8&Jn{2idGK#5T{&C(#}mJC-3&hU$Emg{R;_+^yQ`=* z8TxvR$J|@08CCpd-&eYOqD}w8$!gU94Nh{u?Ej^3)IBKTw=W0cWdCVET*1{G&7lxT z&~6x4*s`q0o8x1Zy5rUBiY{(Om7tRa!;-X5_6d00d2;&_5N@>_h37JIJVSoV{?+w)63 zuoL}56stKsmJVznK9%?hVb|}6HIcGm#Xf-t0E)FLyCZzI6heV@T<$QtPxQ z`Pe)bEqk$$9;8jN42dJ#M{b3yXGG6p8&iB509|FErzioP1kl8n&A% z`)|cSh7Sk0_whnf|I1iuP>!LfMPGv$VzuXM~Q-&Im{U% zb|C9fH&)@P{m8b?wL8we-5b!z^OG?ttaiAh`QqOK%ScW4{P*i?Ag|QFr_ZJYPAt(plp$^O zo}W~*M`WR|g8svJN>f{PX$_6_>$gk|+@F%~o7xmEWK$|DNL8fHZ;CO!FYdJQ<{%$f+kVNO zl6yD2UaE2URR@U=;cxn+ChJZNB)^<+;3emT?dKY;o0IF3AF{Cc#~Tsx{W@zzO|=}( zZ8yTk{S&9<7}YAQo0ELi?xE{C^sFN=cR-89seo+U@6`o`rLVxs{7Oq{-qSFSk?UGi zwqxZp4mVl)AO)sw1G(%?E%;SHF`KKE+U&|fswLgW2=*0z-KTOa-j`ITKA4H6@46|R z(itkUD`ruRo=H)B_)#~CCG!|=As1x16=4LU_eUx4BlftM?W-zMdKy9#!)FTUPcS7{8D z{v^0~{hqaqp)C9v2+{ni=RXzUw}+LIxjAKNpn%{>+K;V9%XihiJ^Qjyc%JJYbLXT! z6nJx32>k0t{q%d(8+ysD)&qLwi*S$B<0m;7!9`&|M{k=$q>Op4;}X2#iA435Js0F% zR~l#@(JVg$&I_!E6iw*MbJ$N^22Flf9swsd+ZLb{xb0QMx08B z|Bfsm2zL`~&l6S8-3R~MZPovN*!};RGXC4gPTh^Y|GadlTkXN}?_ZK?1TPRlgMyp4 z?GA`|Mc>@*>78kVOcmjntRnX0%W)EqScIK!wm}4DZP&jw{}z|>o598njrG2@eX35N zX@kTMIr_IlNO3+s^y&kGS0ox;h9`ta<|b0b4?@SwKjYJ%crd=#Q))o-j?AeMt@Gb0 zUn#Qao5R0(`}pnL4?PO`$mAL9b3QbGYy%?F%#c1nYZ6qLOi5mC|McR^_vvH5c8A1P?cGkeT-_-GeDEen0>%X>`|j5R-#_s#&B;Hv6q#?oX`R zE?YoHHFB-)htPD}ITfS0e6J?}$4yKhM1D_Y;qu*3*NA=m3!-fWtI-jRxNNd@;TD*_ z>NdtAim6IVy~nKo0VpxT-TeI&W8mcxs@&m;e#Rn;>ShHtEvneA9}BX#C?~q<=+z>9 zdh<<|ji&QNzDGYUv$*rZzds^`F|gW7LdoqB_D^Yaw;QMvsG#CZ)7IC!csGB_JYkQS`uY}Y^h%<-tlkRisjaid>$ z@|H#VG(-9n8_ev27v85>ZB`{qyF`Mo&i+DZY%+qkDH2eK3m8|7WEJ-tBu_tcANREJ zbDLF|6H9yrnboQSaiu*H@p5W5c&) zcvzlB352Gt=)QuybO>z#7C%;xbhl2kFO>ebc*S&U#pDSfK?l=~GerXCq`@Yidx5IO`Ux8wcz)DxkLpaKS9*l+)> z4*j1G1phB#6%UWL*=Lgf<=sMjViTG6J2$i-Cni)fjXEVg&G+};13k{+eigWFW(4r3 zy;Hu7o9EA`@quW|fWFjK;BCu>u)Fjr?7->cYIXRarV|8U)RAi_70xX5MU=kQ{TyGk zYEW++&y7kCxTsIano#FIsBKxtBLx*#01>ZdMms#YD$@jaTKb$4 z*!f|B?=o3)1ew_9f{!49+;3Uu&k?+8Ot;^f{{53CcNv}uTlnBF^;F@9znFRWN~feF zgspn~FoG)F=h{<$6$@Y)oE@|P-1bN)!1j5zchhgIRR+GJ68-eh67yN)#5U>VOZSp( zfw#7RAzR^=x^#@#fuyi`!LMYA#tg`NpD-^8mCY{)f?b@KcB``aV9z%h41kaBhI~gwIXfyXyD?M0+c6?@{NQeL88oT3Qsgg(cxrRnD`jg73n*4=NHZrqURJQQa;D`9(k22mpC~t{%CCL@t zAC;@W4EZ0pSnD$Y diff --git a/docs/src/archive/images/install-graphviz-2b.png b/docs/src/archive/images/install-graphviz-2b.png deleted file mode 100644 index 790f88d40939cd2a2098449e9c06be06533f1197..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18189 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 Q6E^1_YZ*SO(XfvEFRalPb^rhX diff --git a/docs/src/archive/images/install-jupyter-1.png b/docs/src/archive/images/install-jupyter-1.png deleted file mode 100644 index 14d6979426e78229b93f45a33b6e0cbe70a96da2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7213 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 Bgkk^y diff --git a/docs/src/archive/images/install-matplotlib.png b/docs/src/archive/images/install-matplotlib.png deleted file mode 100644 index d092376bbf9e55635cd7f939f71a7bd752d63c67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35368 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 z 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 LPzrqe!PEZ*B^(@u diff --git a/docs/src/archive/images/install-pydotplus.png b/docs/src/archive/images/install-pydotplus.png deleted file mode 100644 index 4a0b33f91eacb94def3632af1b2b2f9639aed846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7265 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 D5um-I diff --git a/docs/src/archive/images/install-python-advanced-1.png b/docs/src/archive/images/install-python-advanced-1.png deleted file mode 100644 index b07c70e94e364a343dc79d1aa655098b1f342c5b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84133 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 LsT3=}eEWX@GQo&$ diff --git a/docs/src/archive/images/install-python-advanced-2.png b/docs/src/archive/images/install-python-advanced-2.png deleted file mode 100644 index b10be09cc9b2223fb2f33454b200444487cdc1d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82391 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

PE(akZWAy+G5i_v~3k*oq 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 Z@8%o*LP4$&pijB38vLeTc=XVkUBZS0%k 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 o5;QP^E)4pQsCMLbg9iV!Z diff --git a/docs/src/archive/images/install-verify-graphviz.png b/docs/src/archive/images/install-verify-graphviz.png deleted file mode 100644 index 6468a98c36433c5badf03cc6306b39d65042f1de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8707 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 O@Hy;%sQPbbe*7QW)Eo-{ diff --git a/docs/src/archive/images/install-verify-python.png b/docs/src/archive/images/install-verify-python.png deleted file mode 100644 index 54ad47290ac97e7fbc6f564d6515164626dfd368..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13897 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 RPo4nRE$yyiFW>#;UjPO33`PI| diff --git a/docs/src/archive/images/join-example1.png b/docs/src/archive/images/join-example1.png deleted file mode 100644 index a518896efb5c6ac61f2e15fa06f7e305df083b78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25783 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 diff --git a/docs/src/archive/images/join-example2.png b/docs/src/archive/images/join-example2.png deleted file mode 100644 index c219a6a02a659eef243ddd1e9bb63cd7850d5691..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30178 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 sf+0K`a3_PD`SvFK>)(8cU~leua$5^&;S4c diff --git a/docs/src/archive/images/join-example3.png b/docs/src/archive/images/join-example3.png deleted file mode 100644 index b2782469e2d4814b95a0a2810620bed735392d59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24993 zcmdSBWmFzP*QSdF2@b*C-7Po-cXtc!?hYZiySux)6I=rXcXxM!Oy|{nbI$xaKV}^k z((JD4sxGf<-}Qvb%ZkB&#Qq2Z0s=1~F02Rw0zn$6(Ux7cMj*4PmK&mEj zkAQz*?8G%3K|nB2{``QNX3qdoKU*-K_0^&>$c@Zd|~n zwXu^vp_{dpjU$&EFY!M@Z~@nU%nZbY{|Mq_$xEy*BTpz~>tIaCM$bymNX++hTwPu1U0LXD9ZVURI5{~P7?~NEndyKKbdK&ePWotF@%$s?|FHcxoqvXxH?wuJ1xmre+)%>C z$=Csi?4NTJeVXy9wi=4KL zdX_0KSCcH$tkfkbWCDVBf+uKDB0`Aoen?1QUj(3`{oh|GgbDwr4OB!xTF4yc&+Y$& zeSqdEps;_s-cu@;Ju(Et>~cOqFANR}f|Q4}M914T>g(%cHW@ct&aYK2W~`hqBW!PA zZ68iO++nG=)+*EIXOv@{7CU7Yj8WSlLnXHUJ|LRcmM_!c^U^xS+E2gQY`d}6?h0?i zex^8@K&4b5ZhtU|H!Im7CDk+Dl-M+{XF~dGX%OD79OH2LMb**@baHk9pcWfys&;|oxG03G;V#uVD z?$1_=#bf){&R9&w)w_MXOBDvno<_W1t@MvR}q`xqW_R!2V{y;`IHW3|;W znt&f41tpF|EUL}RZ@mSIvG0-xq zuv5?tGX+NC$YRm2?oXF?6wAOqeB2$0H(jXGIhe|JP3_@icHAE~8mmv|u+6%egCgHg z*7JGViRMpZHShbnU8PtQd7sRnS5(3O{&F*#NE5@L-C`GvfQPw_G+vkiM0$HZsb;Iy zYP1-jEcW)62iIjaxsZqOf~X0flys`FD^7sVlFD<(UOl;1bddRxNTZhbVWds5P%`i% znu7*pEL?BN$o4>ZyVO?)FE_W(Li~aFydJlJ#fOfUl{JB)GOS1b(o*A`xp-E!L|(9d zjcJd~at<#{c!0e5_GmVKY9yXwEfFW8l$ZAv_OrccbXu%GdIv`NBYRouPuig$xY~`H(A= zt5!O+9G_b*H<;I(PT^x<9MJcIBgu_ru$rUXXdTZ~>+!p?F`G6(v2|KqtanPn1zkCx z%md$%)#tmy4wcEVr}LFslPWbnVvb2mgoEvaRx!D~g?`Z>p~tk~TN51#hw59hR5;M7 zW?7IAZ5;`#TkYz#28ZB-OGEgc)^pdEA0>-Gemc!H1=O>qnLAxS8aJ zOCE~C%io~>bpAXBM3?>sG!pNd2@d+)Emb4z*$#Kdv9zF+ZWhx?;m7B@6U#ei4UBLx z1LQ0&=Z_5B@0!}m@dNox$@Dtd)%vN-#?ti5t)JTh>R>(Zj#a9(Ij#6sdrFmx$qTtT zNk~YFWzt2zsp+~8WQz4X<4s@%3Pz+xa&D3cwu`O80WByM4;cDp9hOs^LCT&1~xX)`Dx`y2aRruvhp_HaQBqbPq6L~== zSjz)aN~&L7%tv(El97w5Zc0}N2QFkp8qQ`*6mklrMbRwW5xn!)*M|)z2JJRmJVnSA zwNpm&IiQ1nbQ@4%@O<0~W_mPxs`!36UEn}PG4g0;-T|wgn@=X;z=Gr0BL)Hi-2x}! z!9|aXLus6R^oo4cX+Er*2Q)cLW;TmoA!Jf#H2&5rO)}Yhc*!v3n;bT)KlPZ2#%ES$ z<@hH?k#B~%UC(R2Hk;3qOA{NY)6GCEc6xH4Sm?oQCJZ(TG2cExh92|+NwyY%ACmSdH<(5wd!O$3iM6!+`w6OL5m(OQl?EE zE$I19q_LGVeRKt*34$9X?I|i%DNmAg-yg@akV%240VW01BuZ35c5rh_^<@*PY{86! zF9;=A(DmcTh%9TFFK^)loDQTi->8-Pfo``@p^-m|&1^K><^4P2cQ|*ca@J_TKtl>| zJ2lxT11)U?BOV<&Ib<>n-Teqfw$TDZr9z>;BifP1^k8w{q-Gjumu?c+T)m@OUiNk^ zg=7MyAb5^b#~ruJX-+R8HTS0@hG(7bit#!$ObQ5{ak;)2x;1}zM^=UY949tQ#a9tD z$sO%3uhsw%H`LmTBc%qfGWrU_A)cM9nAeNvF)M%xWec0EIXo&*a#k zRu&9D3*EE$cE4gvBa!8K-U8nN%<2;4X2`od$*JV*r7u34+@H5^|6N;GFGI!+FIQ6x4KODy~qwtN+`<1@FgWzjJ zrJ%nYJG=dKEHf5>y{!wPndU_Br$Z|XE zhNza|)5MzP=L!W=dru9Q$EBJ~f`|A>spV=9x+=%A=aNQ@=Vfr$i`I8=^CAlF-eU=4 zKnT-EaDP0TUv6c+X_X(~BvujNFR#%a7bVJ=zT%o)$3Ut!ghBnFFW^c6Tb2$8%3X%M z`Ze9s9W%HlmQ8_s5#>Qxo5&HJv&2Bk zJ%dtF8k=Pq)yKdYZ~0wuDQ7c&ivizPrZpzZz z5Js}OAoK49?n{BkV)?PnN4Q6Y)@GQ6!Rdl5B&+c?WvkFUz=GNeL zS;?!ruQq({eeog{@RZcigq3omqp?_m5X9rj<*Y~=Wh?ilDh64T>zh$}@L||5gZgK1hIHsAy{O((jNXa-~1|5#j*3r#aC9pC*A( zlS;fW5XNpU(>ssMaZ;&0p}_Bv$^QDKNKT;8WFk$Z zz4y6yJWs3a3c7MUR9O?nI$X<7XiedJgu(O51R_hO$XeMb^NIR}lBc=Yd02!s4(lW+=R*517JB4D_#!L|#3)mgJD>IJ9iQd! z9R{Pr)#Av&I{a8ovXEcZ?l3Y=Jc8R$z@5bn`aYXoMa|mS<8g|Yw1FE!yX5+K*@>GB zRhQvyLoaZ5tB3$qt)kYK5FHf-c~_#4pMWVDjXi80v6hohr`Z7A9;2Ua6CwqSc}uh* zwL_X4W5X|VYI-8mIV6|g>a7g4Gpeh$2gzwn{KiqteHys1%SL%pBbPGk4ADXwp-1z? zW+!VA>;u(TS)DlE7)#3B@eC1Wc^c4|SRJ~y849AhJftU|T5xG15*_mHL&tbB!|zE4 zJ!cv9w=Zo`ol2Jz@%+pONCXks#`;CCgo}2IdkF)O2lU%>Kn~e0BBL|R&qqa0uS;j2 zB!q_8%?@{aR6+QhzB~u%YVsH{3ckle(vm{An3wGHmer*gcs#T)ee~|c?K>7Nli=Yz zxiK>8$0fxF4`fOJx&N4e$#|Bw2s zZIuAm^3J+tPHGM@<%H4706B;xj6VCK{u?QjeXa|Vv&W9D$LYxd6+SvYh`cBIyrGd-BpYaNfSX$IXm& zQKmAy>){MzcJ~G~9SMmqO4AR}Ml3P28V=qOf?|e>GLpJc0T!G2O?#V2TloY_DJzF8 ztP~aV7AY%?snbnSC3*2nDI@)bbtGE21d_!pZvs8W<&4G8_(|I~FS+OzA0_h@qZK2+ zqy&v7@9Mwlk~(s#V$$phpP?qZdl2C?yV+u6Cl`B}KclJd+;(+_&Gi)_t*Y@@HG$+| zJX~x z9wCBAk|!WR+X0gx0(Irp;;Wb^>S1Zr;2RX*^Y)>c$E>{XNCRV*EiI%6{EZM_NCEE6 zAeh@)3R5oT%2DO>3*M*fw>Xi$!5zdeVBfucsC!t9X_-x5a)qacK31QhGGu*2lon!( zcUjczg@fa-Y`PeQ9f@;?jY559X*W*Yx*85grwf~c(+fL}?Mu4P!@I*&~|>DB<_`qWV0czOOu5SJ-mO zAgLK3RklaDuQ|us*guWBcO##S3u!~w$qAYa9{}0YQq<&57{ne(m}P83{Y9-Xcsnk) znK|WhI)dG?e?ED;PK;8eH$VV&CvC16F{@^b9Q$&5J+)hkv(X4rZOxf=K2sfZHfpuB zB7rH_7$*h<-$vIXaMEBREKui}@As+3b}0 zna7Lru^IS5f*|bg`euC{qTWAZ&gA;twu!Fw=vH~z7*0_yzTft;GZbd48*RnP6R;Jk z+SMYCuNssPeKiRf!r36A!8>i1Ke6O-qXPxqvs$PSupv^MKsdU*tt!s)2xDV-r91CC zwO25NU-^_Gz+$UZsg#{nG@>S{5X`7-{|o^N*Djp9%Y>w*;U7_umKF?7Lt7>@ORb(@ zYSRP7i``K6>#4Twlz}=`li};43eNi7I+b&>OVM|I*vSZkvaRXKrIr3J0zO(#CS(Wt zg$B$ylZNvmFaOb4iC_b0QIp0%j7m|F1Sn*@9}X;N#b~w50tFW0MWK}p;*Hi^rOYjJ ze)(0;}**%a>aQ^>fJxHk(CE3c{c{W}}W=HZ7rk zGGziOq`{A*P^w72|pQI^*J*SE==Gz6Ln8--kzY8-p?#h z+xO$b&G}+Ivn_}Arv^1aGG3+mY!R+>zu9I6vs8Qolr+=i_eQhnKI0=3(K_3YAA)Nq zTXK_R%-dKsWgs`$O&kn$gCD+=e8yCfY_>2i^&3O(A&PwOBF?QZW5e`~fmrcd0(-v> z!3z9k&T@T~9TU2Lsz~K&noX*iGzP(UiG95TWyb9QMFyXspUIfyuW0)?C2v9r4*6+z zzSRti$AmOj7@w25C!sUQ1EPgaaDoW873xBzGjC;@ZB!+mNWB&Cpg{G>if;)B<^7ad#M{rpipaHC?$_}>^)0HJb?{RGqZ4JC9H^aKR_UVeStUpaKp5UipfDq;+=kL zZ73Rl?Eq6Yl@eh1VHT;dwBraHBALX`Wo`rX_@u%D7{7KEgkm(y72Xv|c!hk&PBXfJ zKGo>Go#C6u^zf<(ePP;{d#n>K7R;1ZK3~d!X50gnDJGStcf^1dj1^v+m%y_6xv;%_ zRkl^#kQMd-P1hOY9xE)&C1?C|9~^4I0k!^%J{%LS+-ws5YGUDbs|bABAt&EtD3PBy z&(}eWgL3>q((H(_Z@BV@vzQ_9Mo3g<2z0@4SY?|zZpSRQT+(YAbSgvZos*;z9OT7H zn4fm5y6s?PVkKbT*o*cNbb^*Jg!p@o%n*`?W6I5-xzcp!u!ds}3wIk>xxm)^v-W;c zD--0RpSKz1DRNSXM`cX5nlpXm}`vIgzm2AXL<^JwS3E-?JQ?~Y6oTYr zIA*8R^^I6cIKIh3(q*v>TNq_HbiXcZ7RquA6$7d~m|tU-i&8tN?Tqryw^iD=Rb$O^ z5Oxi&8py5=W&N1XUfS!2Rr$Hdi%nbCNj;+E%ay>q2p{Af8PJX_Aho&R$DNr-N!cx# z_-h=`$&C3do;2~CWtLH*P?MeMEEIAgXDXPnc!u`c(=~~V{2PovAyE$;el@17hBJGh z|F=Xo#0AuE5V6X$^biG1D|-@0d#tm3$N)Iy+k!%Q+(hYd{BcVR{R;Z96~h3K)R$sEZh5JoHyQ^wIZZ$u|ZFD**xQdU*V}_z;t23IlAet@np!B%@W=UXi@-0zXl| zwjKWHtQU+{0)dG0W8D{+d1$hVKt(-`C(VGpY9e;X{)^>)_v3{QF*l1oAXWVMUr5^n z0g&4?5RI^ZQuKe`_`e!(^}&+z#SMX#z4YJ*g_2)l}KA$}FLgkAAE#A0+qn-vUnQU~Di~#4(l8b>ri^h0& z;bi`|v=F}Ba>FnB#IXY-2P3P~JHTm>MNrSts(pe1l^PTC2p3(@+*QJtd^vcx!Z*b- z4L4gPlSppOD63#ly&4iO70c-uB#iuly?3sB;c29rR^x1qWZZUzc^Rr{&7$yq#qZtl zHL3{4O14MzRTDl@qppZ8ID%i&9@F=n1zt!<$Z>B}vqm1@_fy6%9*&9)yf|ih;yg1& zTd&R8_h{)_sIH<(G@hDwH*xV2$BuV{!%zD9OSCgoFKRN$oVKeiEax`jt>Wbmf@H;O zLm%T;nj-G$SG)v>&t|5jgADV1tVtBsc!h(n#O|@%z{#%uCiGl?2>lvK^2fh$d=DN# z-q&Jd-TrcV===n;GRVKY)J@VAKD}>rnc)3t|-+Y4#@`RwxG8f07aS z3D_79@mrPE|4QZ@8Y0%*ZL>#@MAXaueAV5R)(caQ(6{umV_PMAf`txneY7tM`pJ3eSN5FRP`F>Gw4QoP_xY%_UFy~R!0n|4jTk8INVG9 zZr7lQCx;H94*srt?}JF6S5GnF1Rn2aD=)8(F=EvU_1Q|TiX8%!E@y+9m=KDGU*)+PDdYimXE3@Gnk6)^P3W>=RJ!regkZ3R*#wPRQmV+ zM0&P2dy_|_D4CS1&EuDJPDgZ3Xu}V+c*4w2{#tJEaF&Vz2d+yotDe5>V>`^f&e0<5D{^1_qfxH%?s>$^Y{|*T>18N zKoFlDCAD0s>4MK=rhd+&?|h=n@35;LzK>_pKRypp|7|gi5frB7J35j?2mP;6uCaDW zG&deSBcoId?0K1Z@<>-sHy^d58M2>FFNM;nGiy3UY$)Uo)m_)~sk5f#%2g@!I?D?A zT?SkSlWh6P_cMjvmh)zS^JR6WFr0wTp+ezD$>r zy?P0Vq2bx`gv~}*rOj>mBH~e?l zX%@UZUMZzDn~g8{g`brldnQMNvR*K=zSEza&|ahz$Yy%%l?AS^ovgG=+mI@IoiwRL z^TY|x{d-soIUb_Lz{ zEg}J_txS4N(mK(*K|Sxg%=M9GqY+z`@{XRu;xHArTJv^z9Cps#(`%Zj2fzVUsot8S zQcg~fs?IV~WT1s=lBc+o;8LbsnxG^80rnI0R60j=S9eSKU7m;(gKo#KPBF2>9EE%j zeSOZneA70miyJra<-O5F1s9^gBB|u_P>PRP+`hK;4m|_+oWZ2VW*~GDPY{f?XiiD4 zbASV=@7>c;-pFWT2sXi_99;6_M)BxJnF3b4cP`s;yW-Q~ngdJkauw^xTP3t5_`bB$ zx7Xm?I%FZQ-k~v`2dh`23*T?Pvc7XVi!Y*iOMxNZFJgRmco*x~+#RG_9SUnDOt$oCN&5tp5Q`8ZHI(tR+gBMOyL->dC|>oi$Eo0lyP6-gDf-6n(lmhKgrTJ5?Qzr@+_(hA|767re_D_WP40x|MV`+mcNXz0g4n{ zO3*W_MxD{KQ$G?~fN87}ZJN1y=dL znXx8nddevf_C@V6Y&MyA#^>dVdOj68sOx&XupLCsEc1GGQY~gs^!by1U^VXkrT`2BY45OB=it?9O8s}~r^TfV zHZ8y*=CT=b_On*z{mon4<+!0SD98DPJqnwxNvkXHOY^nImTq^dhTVV#ok+PhX1#f`^&vm(gKPjL_{Q5gUG(=VUrStoMkd8A;h)>Or%!LRYp9897`t4XO2oz1)+&|eR1N`;b`QM7Cj z)=!SyViuB#9zZn>M(ND0(Eao$69ai7iD#p~RcE_Jeu5?Uv&!%@|Tq3+~CQ2}LOY##fy=&PN z$S(bmQ`kIbPO2X~T`w;m)?Zuf;~~MJx<;gFr@1_;PE&Ggsc7zxRT3$xzgn)9u~bQV zBfDM<9(8L{jAzxpy?T-$g4KIE%tzm>bgwQ8bchkisGD>+Jbjd!8j7-=%HTxB7c+3! zAGWbePEB^P+R!PMt-th&p2*}7Ss6dGttG&%CW;Pg?K|f^L8r^=6~xyb&E($DdY;ZA zX31><-Zt#fWGcHmL~~{a6QW@)r(kPM;gkQ)>tXE~hfNlbqf2!YozR$I;e!!HmrFRr zix?q0J3BFPo?5l1mdLAG%q|@0{sx^EJEzMj#Ep(2Ve8u(REO&n=yUk7FyOf`9ZR^< zDbjhpoqcz3Ijf&0Xtdmr)GHYnm=CAkf{GBBuh6&*z~yO3g;^X$v!qrnskc6n^~TdF z!>>-%xzK%e_Z|HfI9t8(*}o3w<$UG2PB{ey&#BxIJKpfpdmm74yuAQhe&d4CSQ2hS zgpnH9C)cSg@0T&DmIzzc18UgBNNf>(XnI{9AzQ;oty8E`)?J0I9>aIN@rVm9m#{Rh zJ*Z#~U6aX-tCzsvJePnjqlX@s$sodGyn}N1$3ds}ZyUuX^VtYh5kJ%Y7Xym9NqX&; zUHz=M@oI?AgpB~x0*69D;2j=<04rap$WLfEnmHab2z4Khk-Ni-uk3MJaev3@U4oe` z%7!%Qr?wBIN>cvvQ?{u-=yJeBnPp{cvmQ>&+enHsk<;@6Pvp#_S)Y#3SA3gi4 zl>d;XP#IAd(Ys(_6rnxlq5D^b#s@qxHIc~UdKGNk6~p(sKe5u#KjOretPGvTwRjvWyS*XzAw?3w27n$B?>#)i+F%Bd8wpYfneps~4 z88@ECf`dvh79TDW5Bkb4oz6ZX^27EIzkKpT>0@S+;01y>3UsWVp>< zBtZdFYwTB8*=#=8ctFD>CYQx?H!`FrP-MOG{gv)`$Khp5ae@Af!i&GsyIpPFqn*MQ ze%HE!*V9EJ{l1Iy?EU)k^&Otlp3h{aA2Z??9=FR)@urEJxbCK=5>xtYON;Lp-lD2FTI2# zw<%ZRfkj)SMQwq;T8*du-M9tBwdm%(eli1tkL4Qm*7yipnvK6SfAs$Z~-WS$B!r%Fcrz=NUSzBXkHW?lIF;Dr)FtbYScSKM0 zS}$x~s1&ZLDgvYMba&>4Bh)HaIzBrscFy8Ib3Id82^{sj36(JF=JjQ(KBLO!fQ~+pN`UiAq)Q3@g65vlr(!1>AsX(NxaB~dCS8V_O@D5;OGcYJK>f$m>4g5{ z>xV(f<8T=9lpowrH^DdD@O10KLqn;giuMx(gEZyh2_?Wp$^ss`xd>iod z*Q(@r|&X;$p~UlBI^p z2j#avGkmAp7h0ic0s>yAL(|nUxPXP6o8vm+_6&>ks-DPBI)FG3uLTZrQC!~K?I-Zt8|WR zCYOcYcYEeD3w8BHKquB%jRNfemUsHC1Jq63n+cv5f^5V}dK1vt03xLWU9T$=mS8xA!q6Ym#PYYM_|Bp@-SMe)G`8D*wf0OHPWDufmP!e)=D0Ce+#q+R-(;_(5(-G-@p6p6oyo#~$}W7Qb_1=v7; zBZ&jj>1v5~Kp5$GFbU8A$#9sjgzWboVKI+l_!6iBSQQ$#d)>A{-}_CWC7C&@HYStk zOhZw~q&-=ajz1O6X`(sZ9XkgB-i(My5dirfc%oc6J^5?X4 zmr=e?C0BK2Fk$f30pBbmD=RB4tyb?BcWE0?`fy-JL6I{ig6{0jmO=vH*2t!)5NJ_m zsCxoL7#OyjBds!}6b<`PMem=_bZ5PoG}1OJNfwKUgQ&M{p38;#?mB`OHVq2pHL)e< zS%E(uN%%NjbN=t3%@Fg$sEGc*DxL5UfccnC(c091>AFE(ftKJTCc6DsLqKu@Y;bL2 z^l0e+C8&PfK zeuz)mzHgC5N9(>9hNzMW#N$MlF^J(y9quO>dR{;)i*hobO`_9kyWpQU=*v}rX zfJAt-o3HqwW>}5j^KfQll6ZLLz2Q#6$M<$X)=j5=$pY&A3<#N{@VNEgIoSdMMJoH*5R)MvJj|8THK*fXMyZl7pwsQEpGqQN ztdVPTIzlPjy@61x*466tSi3)KuQM5$$u-1DrDf9O80{C{rmf>(ZtP zsZ1Vfs98%d0W-GG+*_MnZFH1dsOlsD1iX$14}}u(gYD8h9p#FJ!#{s2lltu3`Ao{j z2(ZOzHQP*^l8ge4r=}_xjp=+J-z09_h<=FRnibRNg`%k#+%?a6z2$1^dU(YREq^0m*4ki!nUk*f`1`Z$}^__0ii3Y*MhU_D|v-SHzL zWodGx0^LxlwlZj1$lXfD^SU7A`A*a4eeD+jp%G7hpEbwl<+U*}qf=>y)oG*YRBm{G`_A1jCBw3rylpN$iA?5SFE~ zXC?%`FHie2S{Wh^Sca<;I_ujv;>kORg_v;6K4{#5>&1-#U2bWy`~bUmmAAYbV7*e| zp8ttT(e8JNg5I6??@m46bfr>7cLs-@q(~gs!8Ld)42@syE0uN&ms|Nz@VFlLt6dRT z1k<}qHTp_Pv>G1)20Gqoxe$gF~e(6bh}D zVjP&l(5?FtSjRt^1`!D`c_zIyuDkRihOSMNC`Tj3J*P?imzG0(-gU3NL|Ft&ya>bB zb*wszyS|VRMc`CHsdDK$fF%GpEQkQ)LJ%%~#f2HC$u`~WIy(nkTCVF^+tgjLWQ-NG zv`b2&BT72^yw+xI*_X>8gfG~LJ+8o@)$`#TN{G)ml3S~(0suE>D-U-M>%L+P(d{*Y z9Ime6LSBj@&Mj+ekA^(Mq00Vj0TIje#aU>Y}M&@rTBo4 zH%E6`4GlYnjK@2BgBzE6{qh9+`V4{y)aamav z`WM$e7k0q1{W+mi^k7}k`={jpz}VCvgc_gU&SWbQfZlrx>7u*3`?aY|Vkg;VwPlIC z%spC-E0J17+cBgF*s!{uGrnCeoi8_vF@XQ!u}|ll`tCnan_g8akT?MZTi8vjW)97u zQCD&8h^E#^t!^r}TPFn-MWMLeZ(+%m~ zj*daV)sS1vRrz8l!-Jc`%0Cffl7`!Q$9jc~rvh>F&sy^(CYC{9c$pxAVnz2AmQ7)t z_9k&j}NDNJJRWmEmem{3N| zRh>^j5p!sh*O(N@3^b9C3m1NbTn6^s#z}X>Bv@%2o~edfp7&K#acnoGN~S1*JLM27 zO=ptv!~26X34xjXa^4h*)d7>a;pk?lU+gtB30~d9iH`MxIzQL4r=oZa16O zcO>l(M&g`SZ}X7fusE%h#y_}JTf(e%2r1=F^bpmOl(6K%+ELaCkyY)PK!B)L$W*@gj=`~aa($!io`0% zKYx`fk`0$`16}%>ChG+kKY|>aGII~_{a2~dPD(bbITr=prK4w7=)q`}!GQP+Z7>4< zyy@gQFDu?NyqCRkn}M5;057)ArZg z_Xkmf84mgI+x3WLEEaz<5$RuzY@hPKuxo##@m!~n$#|RK$k%ntjpAG1{P^uEcseDD}gkxYtLMsFW_?Rw9$OvN*)u1dR0 z@w7DWpP)5M{!3!YuGt@S%!=>7q6*O`ao9-eM6c)dmt znk^Nt&MCusu6%A6?{PTfvN+opT1Tln>#!SfV7#7>d^zE7mJXwr8*K~!@)dmxcBxv` z7g%gR0C^R0&I7R`$@rXyQ)aQ@CamEdr9s2M{^^DBLCFpz6iImv+-BzBl-> z#6SH=qar@PL=VO^JY8P*}D*l^pv$%R2e@hz?_ zY(6cYZ$9$89TjJ5cY5%8KB-JXA>xw9bPIfbCmsw#vut#Ix>gjrkEC2-+BBds2fw=j zMv}^$`VOCO6|S`+K4%wFi#x2&$KKWaN@Y;h!F-O@iFhts@p9-@8ucH!qN>8M=S<7{ zgmqio^z8e!YGOY}^ADT}ow+WQ4kpm^PGiJM4uNc>)&vz)9N0>C9PJoh(lSDW7px%q`-34~-P!v(@| zfHuQY#BL_l(0Ex57Sf8k>4Ss^ah#0#4xyUu_x0ur!Z{-vx;BWw0?uDcToa?8f;%y> z*4^^czr<3=(W5&CBjUWOXK9~`VEugm(O^E?ajOU`C-6i%QC{u5uLH5xcIW>4Y{8xc z9`|uNejuy_qt4}Q#;5OLcDdPeud>;*O0PR~gWzPTUTV1k=70d4zDe%*+?kx+I`8=z zQrq+>@QTL^IBN@>P&(-5So@GO|4?Hfv%QwU6<=;t%FtbI` zj+eWg54w6u9qmpMpAE(>SDD~p##3>4$vpO#H#m@Nezm*-PA3O#CIyA1m0o1J254JF zVmbf@oLaKrB%%7q>{)qM2T;X7^Y}>NHY+G%ZvT zLeO+}1>SPM6WUJ3tYE-9^)M7PaY*Y7`x{KpV&OVb?Yt9RS>&30axznIJD9a5(rkn_ zKi8vWH?t0h^Ex0LgZ}P_7YRpsZ|WBURNlvO-+!=Inh^f3Jgr1?GanND(#v?cE_vds z_Li5qDjnG+9-S=03tI3!267B$Aj(bZ=KB#7| zXSGycH#sJMPK$OEDij>`Q|oitc7~s;cC+=0_iD?uHLs&)49`14r+u(b3y(_+{W{DW)S*`bb~pYnj~Po|8+ zq+(XyBDjMDfOO%`7=n5{_3-5h)dd>M%dExXtzf1GlGmX>`DKwwY6>|08=i$5PbRg` z^a;{^pC&0AHlLCKQJIDm1G27eQRi(cbxb-xoEhxDQpu=)%cE z+AU$!h^@Z`86fN9rdj258(<9rjt9X(UN<(3gMzm#ic*SP^o~ zFEfI1Lu@(igv>FM>Kb9;#U4n2J<=dzY1)x(TfQn%;m(uvVT3P>8 z>#Lja=fA4;{~rMfL!^IzL%m@$ZI1zTI=!b^;7xJMJ-ob|M$JHmQsF-mWe|Uu4LO~f zuc0uM|CMh3;gcysJ;_B|eS!SVmJ9h-OUEkI{2LdUM8Hy;sTcu2iP^3vSe$Kt>8pe6tz(+nbIvr6@sg^CY zFZcnUcW_kwEcXX0aM-SU-6b|pX7FhNO9ZE*nJDZ#l^+<(pVdX4sZ$An6^Ux@mk(vi zl}2BtflV_Y&hl;@W}Y;D{a>Y=c{r49+rUdIvScY_Nl}mN`<5&bm2FCvWGy>miN`vO zWXm36EFomdP{q}lL~?lj{jVw zM3Fw%C0>us$rN$dP~G?7UAXPh>+qjiWi8qWl4Vdi2-=TKmeYRq7%q(u`mjQq^l z&jcI+SC0^QOVoIdw!4oC;Zc0UTmdoej=y-c1`!4+}hV@i{~Z zcOJL^bJ-~iA!T+uK-y?YFWPQBQ zF3Jq#S|4xPVcvZYL+^rm{|Y0NA=l|4s!CEPY>V^C(B@s*Or885DwNfy=ifKKGXnzs?3E8^1>%;q{O3BZLq|Wd zbLF_Fa-7kxVQ$lKYYL6X;_~yKy_LQH9qTf{wmMRgTZr%}mV(VOhs5C162!bNI@7U8 zZy-_-t3&%Rr0NANN?jofyMesRbmXUc0XmcLSZ*jEhaa&dUmZKvbLrpH4 zN2nmvzEGZG`nq`&etED&awade`S_B%!(ik~p7{^&#+A%aYmQH$k?mthgS z)i|L$b1uZ3QM>Sf*?UoAX+SOHT=;82lW-R;5CDGoDKpMOPb;D@d8tlBHcc2!s7>9u zY>*r!eb=>+Up}@!QGWnZ=7*bT>Rs&ym#FC?y%KY;h>I+@y|z+IaIL;lo_I@Dg}~e( zuF9+|bADILdV@`-z8r(4#<;jaD4S;dd|REQxb0$uk+(FBaAPV-U}2f%6?%Td9c+qs z@lWu^T*Pv{?1;Rbmvr6L@f3d41C-S0ZW%z2Kw!_Byy70 zzTB4Wh!mE>u7pOnXr<5^b3gvRP96Q18g`zX>{Bv!wW8Q$jY5`;+kU70EUQc~QenI( zr|B*UB)CkGy~2HiuHO^?So`>cb)z2;OFcczIIHmlupQRK z03P(#>Nsq%&mR6r$m!Gb6pMX{aghX`DqBVD z{ICYOBEw~4S0WDe*E?IocQ=>%_x87CamKboaSmm!hX=k=uFGYxe(2@Hyus+jfm{H> zCnw!iZYerXllXHyffX!YgySEJnKc?#XD+y%WE!R-lf7S{-GQ8!>il|dWVG^WlmAdg z6N;jI_>@t{dMF!MWB2|jrEMdX8m zM_}QQ-MuNvFbK88Sj5F)`1D+hu*J6Zuuy5GrpF1g7)j-}ST&Hw{PbF`vTf%$pnzWQ z*ixz9ojl%or}&h(Cm#Ede6OgBxJa@c-7Hh0ujk;xVftG{Jh@MRs~Y&UBG9b`u7w9x zvSy#^7SQ&u+qPc1T+4ZSD?NAaSuke|l~~B0ZviUi;n%^E2ygdKuX7$5ADX2W z-%r0r{;4sf(K zv0QzC^I^>%VyoP2Nx26+nnB0Rws8{w>s+hC*3jk#~DV!dx>sqsX}uQvl}XPEcm-f&>hU z!Y8`dGG27{L+z2%Ux2skwZ29V#oG**`74<1ZO8OB-x#t;=4=yur4UG2s9T*pt+}_L z!lRXgpHfueP>+j6$>CdQGxf@>pQk@vpSb|iuSudd@PKa%AO=H1?5X&47p)55{~M^9 z{bLFu2L3Sx!>R1=#h&LI)P!=sp}lbA_a39Yd3b%cFPnC(rw;;$0x$vFDaZLJ4juQz(`)3@aq zgVs*W&MPfxC%KRCAW&;c;`ww2m8B`T{nD)!L-(bXWC>{Q zUaH8sj$P8fF>CllTn8k9!8h`wNweL-t6CDH{Gk%ReXg=DRRwjg2xh@k-;_PBV?I1h z@9>P?STo^}K&4mP2PF&G?nqE^yZ511tI_o1Py`>TeafW%b>w9qNvHJ8TV49QyQk{H z7||A3qXdy`Ee~IB$(wc5yoRAIiRZM0pbe@QRuk1@c=Zyd)vNAC>J0xB0XLTlx#4}b zpP)T&k8`|2C7c+-Zl> zLKjjHlT=Qem;7PUT%@U$aI#8Su(xQKgM{2g7CUL1KI-I}c%#Qs@MwlxT^pdMsmrK% z*`7)6OK_TD1TwWS$J$)xw?}ub^;RJI4J1zDuP=NSNI#9(e}IXPDGikwLiO2AXV|;M z00%!t6_j1SxyIXq!Cy}wdH^wedC3+s1cAJu_HMx zj5Ybi56kZ#(lbs2fK>U7tKtNd0TTz{kr>>K?c*;V1peuHq{5A_zZ&^Ni7p$P03J1j zpA7oPuls?JaJ)XdaEe(_MgnS@KDY@El$*IB^q|WW@Bufl;KoJ|j*XvZf=OQi-c3`| zSqGL;trbwEJ6Jr5wj9lPbyUL|N-EIDit!$i2$R&YAKT+l40a14F;S1%%n- zLJrFL$lA0)t*@(i;)Mtl6}u=~T3o#UP+Hz$B&Ri*l^6FFTAK@UULl%}Wa>m04)=*I zMl%Kg$YG!*QR<2oq^>x!1%J)&o2y5qu`(Xn9|%q@Gnm2oJGW{GCh(Hw5fdzpFytbf ztlQvpU#E#&CBQ@^kB*SwN;)E#;ON>&FoEH>W-1B389(favrPlOCAENZ30FF7o-549;MzK2A(+WIRkT# zj-0MN&bkG&_#DUI%)x#ZV!yK6$+W+-9sc5)`_8vPJl@TtAf7$A2y5mMatVy|{jz@tv7ye)IOYXw^wi80?RoHEzBnR1V5V!9K7? z-}C}PM{A}a3HAq_e7+Q)!Kapd?g7eB#gwkMTcb&+tKxoo*;VBmF|SqIt6Y1LyrAN-JG*xYD9a)x z`kX4iFHa!dMyS>EdfQoiaj9z%R7~-#Yq!oSrsLbIfLGH&=y;9ZI#jfFi86@W6$htC zny61J+4hgSPG?GxxL`Yz{)^#ro~z?0R=5TV*1oVsispzPEat9$8!Nu50?npGO5aVCbTmDjLe*3BJY4Jvgw>!!rmOg`JU# z`p?8BYirFL3r_56zB{~t=k?||_b!TZrAWcx?Qv_M>tFu;bfFgWF2fI|Eye0dX&2qE z)Pn(zyH$^8s33MXmoglWFat&{P6|>0y1_vbB&w}r0`$uBxgH9jWV{iRAc2ejjy3a! z?)A@V*(ibAkgu$%Ue!Fkjq)`Og0;ah0TRItF9aFxvcmghzdv!0thYxM0Hsy&SIM7& zunlNMJNKV^t)Jn`z&EF0#L&Y1lIZ(;!i2>vVhdQ{PhPv|j*YX4li8%II z7u_pRvmp(gx2Ttei`x#B9?z!1syG}kvl?}tZGE4B61aaime)uv0r(pn2~osdGJ46b z;~;6Qrs6r>L?Jf$)N&h`Ic{{2q#OALj`R%c2bq$N=GfQ4mBjz5%9o?Y#al^uV&=id zKxW6XOw?vz!*#2@0yvs90xS9OA6rFfP@CPssD`nV099-|UOr#(7k_RSxj%oUnV$w5 z#ya%ThB1n5T}HdAPS|;|&kk%Etv$rQ7RXdP;TwV;jKig};nfMK!-LZfVzwjl3H!Te zzm60QOBOW{<~K}DUvW0mnv~!1EqKx3Wo!t8*(D9nKQZ7-OG0W{_=McKC zYlc;rCF${nqwQOo?$hv)%2i%p_l-G8?}I5o7+Ewv?zOHwx>1;%`eKK|nnk*7zo{b{ z_#+*!IMZFUPHK8o`+tP*|K(yz$$C(c)`wKn(QqU|LQ;Gvw_9}xCL<0~Yk{2n=&W*Z zwA(?anN8zx&G;RV1n>ulx{zNL5z*uq$#h+d2ezsb>jGLJGeWngk!F^m6*Rm==NWdjY2$n8 zALVGSHHO5hzZT&2_Q6GSsFy?giEmHOvy)yPzT7R%LNPx+0!m7?#eXUYba zKlPi}F6H`amAmZYGbRtEc9l}c|oEM^STNeyl!>sNYcaF?+ei23=)c820H*JnD3k#?v^l{PpdIO!} z$ap$Ux%WbxGshJ3B3@{$9I0OhU7%zc#{H#eXzP2W*QAL(=4P^Xjo;)jD4b331N{Zq zsysT_7PAzU|1=4IC*^Z)r@ji54F-i&9zDXj`>cP5yzYGM`FiCN*M6sQbIW!lM>?0R zKv-S((aDEs@>g8l9L6-dl>s}IQollJ)Uq=FW%^e>7pcBdhcu1cX;^#UUccesH#q6C zTH|rEsq#6G8`@{TSwX(%Jf}v(^Wva8zaF|5j}e^>N52gGRLsLqCIaVjTm4obHSp$k zI6jJ1%6!4SKgan?v{QFhNuWP4OV#*_8jS>2O$cb^ zXqLhJ*a(|+cq(4Ps3|nwAMo_*PrkzX`{Jk_AokocRM2}sHA(RBzC?TpgbyMoe!mTz zdLI~wJYOLRM|#;I0)!qt=l+Zx9Pkyi{ zkB`^}#{4z4n*_g3e&Qksj59b1?k{pABkU7+n``v^O&|#}a>mW>cN}^0QUW<>;=UEn zt9PBkpWu{^(;NkSWMmt*Ut3yuVOzw5^78W0(Jd$3?FFJg@s(Adk+qONiZQEvNV%xTD=j-{O^J?AqoO|xQ-|Dqj zSJ&#^d-q1w^Ze?m9U?0&0uO@)0|Ej9FD5D|4*~+J2wVi9J_BnEf7~Ym-#+U}hzNpw z{CQ<}6vY8+plw7|?Lk0ji2f{~E(N?!z)DC5F)1O)Z8&%|I@Yz>g;-z}rh|}*gMhWA zrJvpRT!T#P7X-TqW@~tn7S> z#O?hA`a@2HBxWb5kkkq)6t>tT^d+;9jFOs~TB?GJM%Bt7keGbCdpk48GzVQzFE92q zn_v$$I5O}0>|p?IkOArw#o@t|%jk|Tqy5FgTW8ftYn6L>j)eF#EI|Mg+Wm(C1{8td zUU9<1zs1b6nH)_E{|=<89pyV?)&oeneGGUVKBkiK&Gz|QbwNgYr$ z3RrtI_^~!=Zac+1WZ^akpshPasa}a0U8G$gWv=;}jucM)mXt0oJ^Q>-?wK?(@%H1q z!*3`SZCz1#s2zzejd=iObY;+R`CydhBm}D?(iU5P*mYbn!|w6Yych$pBZ;so9l5Vj zih6aNQM7@QktuPGZ_Ms4<`f)lr*bky&5^YFL3zRC&+0BDbfHGE&$(xAPN0;u_5|@A+jE|z{F>F=_2I@Iby2F< z0%xE(A`qZF(_UADgSuu7!c6a+*2BR|wqr%U{-80lny@ZKYgez6rLC@w&Zeng0L~P zrJyV})?&%FKxJvN;mii^UY{KxwJ$v%g|plcwNoSZ>PFc#=@21aI<)qhk_Q=i>3`3# z;LtQU-f6h_zrhzu*i2N~o_rizhXcw#M>6vUx&a=V1Y?Te&q)qfO zn+u(#hql^saY+wuEu+`|t(Ldn_-klMn6`aZg2jDKmu7qI!ujwV>&$9!LYgPc?U%XU zWqqEopjFbxImAab$n0*zY2-rZ*52o#_w$RfCgvEy?AFUhKnX;~o z!BA)OJH7XPt%oI=9*#SxjER*k(ngLKb*Br408`JTkWrky@QgsrhL*tdgBzU1FI_p0 z>BDCsC-)2|t!O;x4}duHMQfS?Wrhn_>XzBxhRcwn=N3P8SVxWPq>w(mE|37A6SwaE zAK?}VU4#62oNxmZwwnf|ZX7oj8Jq@W!gZE!2kB3D%FIagbCD9e zm#FZZQM$EJwjZlQq?GK5n(B>T-bO5Adz7p1QxIJ{YAZ|8D1zbPlKcW>d6w#<|lICK=|z)OuL7%?01j@9bbh|2_E9CNo}@jkf* zW~Ogckn^yc;-a#UU+5~KrlK4p5oE|qir}0teO5$2Qsz~UJ9K+MU98!L&KzO=_H>Xn zcN6=lj;`7tx8vl51vq{mxL9)b;V4dX)Y}cGZ!X^a-u9D?YkS%B?ErG>2Gd8q_mo@G zZi^Cp3Nr5zKHCWUGdy_-(n7D>CT08nS$Ix@7B=IH)u;*^CI(t`+W31{UJ&d|##erx zD1zWr6~y$3Y11_mBVD6n)j5`)TvgUW>Wm4#lT<#MWn6;Z)e?%mOOD`9C^BvgmqOO6 z%H$tSMrH!=t&N!X8U1xO?|kLvVC?j#TrMO4^<$SwuO0^q3{qMuI?W7Y+xyIk*s0Xl zy#!U@hx2_u0s)j?05wnZm;?m`%7UQf)JPh~Um-0*=T*i~8m@(WAEp8pS@wlt8{#aD z)x{iRz8}hUE?Xt4Cs(GGiw#9Rbz1@2!Re0z7AkIsS|3Ne6{g3h-LN;T3@e4>dD;uh z)R;`GfpOUS&5oYD1ah7!3q8r#L%~bf`iGe=g-saT;xHc^8JG7AQ>*10DO+!dNOhDc zs{wWCZBtqANAzEfQY73SIVBNj4d1NZMkh)(w`C!Qb zIx04_+_0rHCdeUB76q9JAAGOcoN48eK|A4<5^^U@mq`MQQj8Sy$YC=~QqOQn4wxej=H;>3=hf+$l*HaW z^P~L!g6Nb4Q4cdx_9pwM9glBWcypIOKa5E(@cJPZa=~{tPPz_HsVa*|qT@5gJv>NR z8N3MTiPF?tn#6c+p>tB1CGEUs9ql>WgPgYpTdiB}R&1T{&c@U$q}R&OZgKdMUglsJ zro}$TIeT3gXdNu@8#d55`l!$5jGRU#Z_7t4lO)2Mf@p{gQ~}mOIcHA1n7^|F8rADW zrPvt z=d6dkg)gBD(BH(>87#=@FO8;Ht#M&8dve&LcT5c60|JBT?@+em@Ma!c1rpBzh^{G3 zkDA7`_EsTvY6x+I^+0M9$f|Ck5d}imOv{sZA$bPy^92T zM2n>>NIA)tc@&0QH@Jp;<2dwXM2*uyE!3v=F?$AVi=!?22LBj0*7@CKbiHG(g&m*_v7pDRw@P~P;0`ad7Sh{pTkwQ=#uos-NA{CWMT8xQbwB_c zTmkp^33NdB$!}%6F6h~KqkgOLe0%`eNo3B?%bO$F`A(j+4-frTt!RhunJc@oc%7kW zKZ>5%DLDaICy6v@XH8SD9wU>GMn*rI>=u@WvC2|iPdrjjn$8mf70={&+?{C@`lWQ2 zwsP}OG7BCqGr7Q{QF?}MTpRp;aA_Y)-K(d6oLc@AF^d>!6L8u#hNI%}8=xV(_^mPMh)=}EjdS&!A4pAP`J_j+?I(Uzt$J(T_;vvjw!$4+Ew zX;)x{++jt4t$)rKUsTA>ZB(JxZhwXhd6$+9b>p<7GW&tSL&cW8%*NZqUX}e^+clY1 zgvFqNFu#>!P%=*rno?UrH%|Lx_JZ1m$1j@?kT(pJYtj5l1*4xbsPt381S^|U~2 z{e|@6REoBzkw<8Rek6IL><4CW@@#MWnTeKn3E6yt+DNki|Dj%D;D&WZR=4deXZzB_ zlHT6tpV*ge3(13D)1%u1kNX1_7ieV?P zgeop&hs7AzOFI68%d;g-x0g@M|5+-F2`;$_Icq% zmeoSK2~^*ow)2{@#2@1(vaiSjE|i|@i$WcO(a-zl`^((9 z2wD~UVwYSd)1yHJ`YTeI^i;>u$D#&U&tSy-0dP!LIc&%aA*x2W?VC$K8JZN``E2?L zcIV#>=E2CB{G~DzJQ+~)USXYF4Sug=1$v?%%)n-^pR6F=onf#De*(?jq8K4^T|#AH z>XLYgRoUxD(t|_Rl^DYaDYfZbKSYvrMNO4FBF}7p>w(5kb~Ml76D`%-+f%_SP#o8l zSNOIj{Qb7x_M!tAuJ4Yz?7xM=Y%C$$F{*m6CX)hinoT??%19kY%d4HwBmsrqNbOrl z3ngZFKiJE|u}YB=$kGlSt7_@@e`18l_}(4t)LkZ(^l zy`tWt z!b6Vg8`iYJlX~g$Xvd^sPw!$Nmg9)WX+sg+==dfmW;R6Q5E1 z-;^$Xjm|Kodt<`ybz_5 zNVhY1Ah`AE39o7XUW+J%Rv>e5=@+@!WT(G;ogrw-ILKVrH$RWAP6^=|lvwOIV`FY8 zQ)Rdj``wp9ARd`m&>P`5bwvZi9*aH=c=>Zr{1O zOB3s0LoYxoiPIUkO7$7!^qt+WEb>;Qbyv>M+%RjrP@77|bDjVzb{Bf<7O-d0*9YQ6 zb2;SV?oaN*l6U-|$bJtBdij}4B79)k_xiO-&+m1!12wHpDCZvt`;x*mMvaY&%W86r zE0fl99V2W`q(8e$jfLq-lsG-mtw_XQc;9u+4~gnz_o53T^w#Rn9a1o_nw|#)na5`Q z2?CDb_ipE=?hPqEGKtn|CU5x^J`z8b7PC2K<=iUx1jsxx3>)yedP`v(UUebiY=eZo$I%!zC#VuL>+|DKVQ~YEWa)$+BM|$tK~au$+UkOnTkzM z87fIwlLJ@%LAmsWcHnU+```pTCWzM^z_-A#&Sc3&AHRF?a5H5S0o-!9;GJG=7S`mK z`T@boBbhJgx!^d-S9($M#?I8#Q*_8(WxRoE8WhG_n#^ha6tJV-p*!M2^G^UW_Af6o zqiD+OYV4W&foe_^>Dp?3TVCpL2ecyuUXOlX<$`o&G9++rZk-EdzD>E5XQZoNW<%zP zW``n^H%Danf@i}*@5FbSMA*ZEatwPg4v)6;fGpoxSD$vd*1{m%oirzpqzsuLPV|=$ zmc8DB@J#pHpEc!4GB4FK=4pAd*XI6uiL|HALDU9QidS#Tk+cDG0JY%FrJb$k`HCFW zc{o=5!kp87KsO}rTVCZdQ3UIh7?dVh_w!=k!7mchE{mKjY<8A1ou@f?Ok!YXYCU5Q zW4D@z!w~x(m0S77r_lS7Shmm87mUnTa?ytX8Rm(IsQ{fZ=U&`Yy( zG#ASbWvIE}gj;AwkKZsDcC-J`sw;yWkrAphFQLV?r2a-37$1+yU^g~-n=IAUk_AO6 z?=ttsvvpb$A$e>P__RhPFYmSGl!rHp@N0iIxWiPooNzj*1@a<-cyLIZfMid(`t0{< z1X(lE1H=XV84TZOr#|tpu4lK(w4oLGRms63zzsAL*OGo@>>CM5V0<`lv0+ZmKtO>7 zr)}UYqxqFpCB*}>*aOpp!`X5~S zUjg;s`}n`0wqoV0b{aAeVE^IW#mHuRZ*p~=8K>|B!`~i^YSS6J4N`#O1W3XENSS~A z>Az@@e-FlUKRxd-R+lJ)*SRsfSUn*|>A=&}7MAHgDGq-ttlG>&akx@ zfmq`v>OZKAaK0OQvDXQ!hsSPTC&A`^TOktJ6EoZj;dbw|$E;a=G!>rIB z4wM|Is1V4kUc>aBb6PtBH>pz}XQGuGF?nQo!VyFfJ=Q$I9SRtQRd9sjY2%uVoEDa+ z)eWWoJUfJPCuEZ&B?KKwS;PupchH4l#Kani$6Ih`5QmN1__lRsz(fEv;$`$3_+V49}Mw^1yg&VIn(qtRkNo8Uh;S$@q zKSkyn1iO=N`}}aEs}9GinR(^cktuwilSN^c2EF)fAB;*SkWg;}tkdb$GsFfsiYnQGBVC>^;-!^_Jos$gM?x z$(+p-?VB6(ioOJe?!`=j7X0Y((-+nCi96HuRff+^g)-RBSy-yuEUfWmB%0%4_c>Sx zXV?~x#*^d%yg!qeF<_ze%IA^Hnbm|87V$hBFnAMtJ7W3JmCQ)hw{0Omiiybadd|l#sU6J$Fr97 z&G)%C1fGoTi>ypnQxn)Rfh3kDe)DjU50Ro7&!M-06u{^<3uADpye(OMNw=(XEK1$; zaMqfnro}K>gxVf!X;D{hL504W?pB|fVd4dm__o(`K58EKbFGO86Q=W6O+!tOc#BZ~ z5}($UMZbOBeaiVxD`vN3m8sxraactJb3<*FUZHCQJs!xxlIvma5f^uT2CR=;0HReW zYRLeF3?jsrO4fAzs&G5<9#jOEM>oA3TLFa+Z{%VbbnXGL`4|;)IMmh8QNr+tV5~@$ zW$lJS)W>7F_HNZ*yMyWgv6alXyankh6E`I*P;-g8ERi!%5fXYGNn2U|^O(;y{ABfX zRC}kK;i>k?REiF%AIwRy3@u-idiQ2GTI$WwBSSO8eZIiGEDEv}ddrN~g+1+?kz(vj zi^_-_h&==~zrg(fr3nzsWtQvNxOsBT{yrnpA{OH|Iv!Q*uN%a><~G$3jxo}|LnIph z9_q+bK_Z(~K5~Kg3T};#=zp%CVug74Q2m`aXF9Rr=mwilVIKmLzV)f2P+_f+acPSe z*D(;oF7C3S3=RWogLbJQ8*=`M%-7E`%Ms^w5EcTcT)i!FT`TFXBc9k6#?c~jrV<@52fi&>N?#Wmsh~B`?U(lyq|~$7M>y*##A__C)4!DP5SfZ<6v^K#|q!t^?_>}wMt*C{L zeJNI|i>wdoz4&H$U670CR4qVG@*-;Js!XrS%vX|rv9RPddA0_JIu)#UC@e#H1ggn! zhr>7J))Ib44vI6!n_}Y_ttDlwRom< z-N}dT%vIhd_I)-OGB2grpARbHmh31$&H;J8ygQXFUA#7%tF(OFx!1%y8_ZSo5@QuM zCn~d(Jm@brm4an{$s+`t0>#r0no$Xz)fe5;UULV_MhtV=6xb$~Eu(z<0>+{Rff{7h zZsxMwGfT-5KyT)wP>x73aP!7GO}?D$gnA= zY{e@C76as1f2G)~#b~4Q!{pSr*RqX$gr3l8^^+k~&+}Xo!(Fl&mjMho8eda_l)E>u z(e67UWT}=K=%^_o(aL_=WUcrvyZhu7T*$;4q~lRzE9f@v)d6t9_)+BhSnPWXZ^1f6 zR>s@r4`KknYzb0aQt@(>C@h$qm; zEKX#&Ta-uKieO#N%QpTf2la62jK}AFVhkFy_85eKo+ay(iacM6!jpAJ{O)L>sZ~)v zdGjVNk-h?wZhThT1t!D3XG3B`+@q|?j5)5LWggWn^-AVi)57=r^=8;O{J}G-7f{1o zlLRyGe8PAM?BXFEe$#H(afLd(b!5x%GGITb>K@A3xV|`Yjc_9XanXPyv&93px@aDc zzQAv(Y-I_x67qP5vsvayck^^WA_Lyn;o|9iKHS}%5S{PmRZ0)@$l+#a6Z%~=m&rIkn5vFVXvO&~XOEc#C!Zh#Dj_NWyVR1=I z%Hjl z#m>*yI&dOlHS)w88G3k`>e_9`+HUzCgU|E9%AGn-#7dbGAKsSiNQc_>5x-+snS$4~ z3>ukC$!<_UYJXopzu2p6!uXDaF>$WbDPml1_Vz>Ei~;kRgXxSXP$`D@W=F)o`n8R7 z(yjSQPSJJtc4!)Df&Ghy^fYqKF*O&*qOKEbM*OUSJcCzJp^&*-xVl)^m7;v-TG!-$ zsU2-j5Y6`^TA$RrGXw81vceH!mt985^^JH4Io=y#_N>U^4%4q*L1$*=Z}wDjYlT=4|N#8^z>h{qrXJp zF94CZ4lwEf^_P}Ri2m3iZ@^}X{u78;ZLt*?$JDKr{0lP2pqad2!=lmI@%WsXXBm;_vu>hWeZdqX zMNem=#mMyx68+GlbplMn48wSWuGT>!`&wGzAU!1$09iY6(bSE>Aq-;0TQU+l7NXsC|Udh-<;sT*t=mT{x8L>J}%a z)r~aQ4FNj$NvXxgix|>*^0r(s{+gAa@D{BSz9t%gtPpkyXP)Kf0q=tC-)VWWQmp#L*C^h5bee z1xON@RGLl{&koh&K)CJ!-E5m!ETbloK@7gc^*NCUHzRwIUX?*9Ka=vFZrA3;@d@zp zOmCUM$#t?Ki0F55<50?<_i=1$`BZ+(Tn?NgG@hvjym14j|Beer|u_27_zeIx+za^g*bbe6X*_NN51-tl^Ell)D*yic@RoG2Z%_TmP|x5AiZqFIS#6pFzV& zG#Kyyiu4K&3p}xw)Uu%24DTz6!bT0D9!>CgL4qX30fKW`Fhqio2wwgs>RXO8_D0Ge zY9Mzv%_cw`0iF*WknnwlVNU|!P!FY~q^m9AGDoA;$Kqbuh2ytt zl~0;Yneq0+g3$7?;6-uA;e@e<2T6B9i^48j=Pp$pD5^lU`gz`YGm1K598 z1wqx=m&q3)v8?%2N=_ixk$hV8+2PX}{TGL4g@C|AAj4=^akFhtOf>lly*Y<(tmKQR zINsiB#uA}~p8SD2kPupa<1J%Hc%PL_S=ruT$2A5^1Bo_HOa0phKpk|Gw1=e3j^!-O zmN0p`fUoQBgOE;2S5M`N#grRBpfI6xwg{2+&`Bs!)9q>cDW{Gpp%EInIQJ8bmA>&N z$RU3>TLG=w&MLo8B*R8H17j7UkJB4A6{k&Rc{&)rlg8-Ys6l`Q&)=zRdcK*D@H{S6 zk!82AEuTu8y`S*t(P$xVbqR2oNK2hAj6EQW--M@%xIhccA|I~b(g98px3B|HnjIh$ zkwKhY>|$H5E0)LvXjr-#{~Ktg)ZRPgoJ^TbM7|nb-1>*$=Ddi?oKmtmkAULWx|Ty^ zlNoJ^CT5kaiY5Tz(=1hoi)}HO;I)tmmQCcEIxPso=+~2bR!R>Tvta3F8P&n@ouPa! z=q?fX!yYz=+VZ6oX|t(^+;w9!**~P5J4W#JDx+MRK50r|^=Nh#rXhA@t<6rf%p>0| zGM_UW7KRK47t&b-EHnJhN&^!j)hlcPYZx2agU%h-HX*qLcqPtWo-ht0GTZ1a5KLe9 zXC|Hu6}!9!o7h+w45aJ#NmS{r!5F6zT5~MUfcda(hjCXF9E@HG^x?W&UhLOZ_YH%OL}AoB#>lDZ3gkF550!dj5PV0G<|pleja^I7 zn_%Ya(EA6kIOAKxjkf*t0YVImH4#_DOFuT7AcD^iqF?XKg5Y0#KC!3I5y%}Giz@mR zcJdR$X!WehDQ_cFc_vl&3W46HyWBbYQ67mQ0tX~MpMAZHrL^(O&SsKrPnI3uS8hu= zNjWS2>J^@wK$7T$#e@YP8G$wR+V`!tI(q>UpN@mKfH_>70Ug-cSP)uTo=%!DEvl+b zTP+hD%VbGjf=9SAdv}aK*ZI5DkuWv#?Mjhh0p}Wh?Q*oabuIxI})cW6%w6w)S zpkZy@AWP?Nh!MlBpaY}{i4W7RTFv?|flj4ASF^&0Diy5IJ#2Qwd}3Gqo}d2;)5x9N zarFsfyK}Q6M-%kdl5@Bqd%055az;*Oe-FoP-*r+`ZGAfjjn10`EJ$r<@<6J1^A9C~ zr1)D5zO&EpyWe2sK08~j@&RQ{>3_%?ijY6~&i{^Q{Trb6NAKj??R_lA|6MrAQeH&; zk$%=ta-UmR&6oVI~Z3nLtzrLKJyb*$lF z)x7<3Ht-mQh>uETaK?kabjqIgacXNP>r_I+(#cnG5AFz0Y-Qp02h5t3!o=#R?{|{% z`ao+mB-PEk7K#8N7}~-9j)+0QtT>lklbB%3 zO%~DEgqE)f?c-OPlKvVO_~82+Z7nN2#qIstiW|F?OaN0XGH7eR`B8+w|dUCOIvY8s)+{3M9zhKQOE1YsbFhY|mGm`%=* zO=qq5mTc_BO$U+KYGrKcWt4!z0NiwOb8D&*>?B9$?bY1lJu@kXlhA`!F2TVviO*9q zRdI!v2Z9{#&TgP~!RDEQ@7Am_X*OWGQzprcnXc?W#K4tzp~%*5Reg;DCl*Bwxs%#2`~)bSaCl@^dx;qqhO;_zoZ$nkmo4De3T|ILP89ofsTo4v{6!eqvW z(d=uZxkpWDCTESFG2`Ed#FG97JbTC(4IJ{)yz7hlNqsnw>S!IwIg5b2D==ELp49Boorf2Z$?%z+^}BpRA?*lo9bcOu1TLOr=LgVu z<{=uexgf~CQM0WI)@e_aTN~+*5y=UfFwBql* zIkv4k=}9$n2Kt7_5=gALT+8C=1#U9jzWGgZiWl3F8)kwL^P2?cSx!=M&-PLHpL-Ifvu}e{!aB|3$Hv2IP7Pusm4CFbD~;fP;$Q|8)#06U(yqH-}%7Dk-{@zP9_p* z8MR|gLfCII^`%BmFtbh>DX(5JJNct_#Ce?aeGBoKY1XZeBfNuZDBP%SBu#sJ&dWKS z7dWuxi7#=~a#)P->j?Xpp(ee4AGdzG8dh=|bHy7GUj>7nHKYEE{S7$$FrPLi&HzPT z_+rz(&0TVJbpZcXW2rvVzstJ3r7@KT0j=Bu)&*OCA*+`KibW=1d0$jkB2>n{$eEfd zoO8mbJ_YggNaq|Vd9!k8GHZ91tT!74_&3gSgn^k1?y}0tX>>s0tc4Y{yQ!h2pOHV z80pP1X$glXl`qO|C-ux&&KTY4n?ZJ+svErMse+gxGW}qUPXuvtko4tx=_}PF3c&PR z&-|;XA>v^)Uq%0QYH4VTV~9-PqS~AQ=+xBu|SIR7fHlAmq%E0Y3MEDr|A!(6Na}5z5B7}()l5bC4@Z_DP0-F zGd4s2YGRj*Kb2W^_%!Rs0@JnNp}pILt+?(X+oX^WL7-aUn0gA(_Ej}9{odLrp=?0UwZ%q6jHGT=sFkK2)T^`>)P#M*)x0|vqAfdPAHzW&vl$o zlRs^G;?Hl-%0NmUUoIGzq+(i}J9JkPK-*>4dDIVdB~8HdMeNK5lm!p@Fc}hk&Auxr z1vNmfuYqWO`w|m#x`gNg*V9oO8B@S|0k{YRLwZ1{vX&{XS4!fmNVKwZnDDywS$2og zOsJh3hT-0*s_$)q?{b}|fu*{PS3pn(6TuodL_-9|V;e-aNZ;ey7d?2$KJs|0Jo;TW z9z)UcdOxkbXwR}>ZM$8A^ z&?5ggi1C(%o^ovTFOaDk>{{?L!(_1>FptIDh)ar;X&I3q#IE*$)pJ9$?A)reQ3!2i z4yTDM9^ACpSxAe%Yva}jThCOf4x8pyfBW>yrNb{_BND*vMOP)3lHdqF+X;6{Xsb5I zgvS7i$XC<`9SSMnuH=*?X-?{`(`=K6sQny49!RtQMiT%3pohu*7-Y0JS?h)y z2xGS(^9VM*8bbt>%qew64gLHTlSZ!uG?{C^J)*`{=AQk5IFRAi(PY^Zri{>ge<14f z+uN1@d{0DF?r>hAQsQzPLBIZOcB?@=g7wvuqQLhFnB=kLe`wONXD2(PK9m_lWUgyW z(hKWhzv3*{ymT3ny>&_9zll_?7){cw^PN?}jjN%hyY+NP$)=_GL~lt)Yxlx$Lgu{(5-8Qw!*FmffjzRuGR4 zKl*P2wKG&J#?lm(#!Z!q730Xwhk$dn0*gCHcZxp$>h%`UQ{9dyYD3I{#4!r*lZx>q zr=TIDgs`0DD0FnreK>C3&Y@sdl%poM3Qt}A>T|)5pkS6ydak38jAzreke9L50s6kX zyBod3%XeSY92IjqI7An*geaI@{iJa@mu=#MNGiI=(uWJCN@j_FV)N<#g^4ybbPbJF(lqSM; z7CDNVAee* zzg*Yi|4_}o*O+K|-7^szAKjfDJ}>qy&rUe~m87q+cQjGW%LMP!pOJ1tQW<8QrGtXLeAE{&BXxKNKza8rvE2C+pU5xxKbYUzrGlL zy`Ob$_z7gMX8&iTY)3K2XzTCkuLjl@^w~06k;Y?k0;H^({+clScMN+F*mf36{lr2y zix?mhRQ>m1Fq2d6V6mSt2A+NHJet*YOsmAH4Q{MOC=Q55>AL?Gh#_Fu+f%}GlTsNj z0Mjj4^Z5Z636_hBx2wTP{m8}JJWOzS7chrQZ-!r=5T*Z5=vTmgt}U@|1_^}(r&^;$ zaN2{(KFx-hV)q9h9#=8HI9{+D^}GAMH&fDk;EN@&NhxK0`J!+o&xeoR{X;+3?7^$w z*=N}r)pHm{wshZxc4Oknd_fdo6FxZydwFW&3o|GNbFg6*2Q{T+V96Mgywpb*`>l=i zCoVT8ule^hFadTan#@~sIVuyH0Y#t@5)nG<3ll*57E8-N<70oi?gnb3)34G5P~B3H z<#g!w9OiezlOeqm2jMZ}!|%_DGaMUtkPqv`xZ_dqujRAV{jYLh6u_2?C~wj@9^Y)U zw^AT~FM^wTAx`e<5pimBaWIm7AoL-TV!pzr@%we5KQ}eVFQU#xDK&26Nc>zA!=Kpp z)0bK|In7Pq@$Q}(+n^?gzn}q+x}5O;Am3#tons@p(H6>#0aCzOS#g8D3}#H`16%G& zmYj8C&lXI%(ocWtAD|~;Yh$5bOs~E8zY-_lC=Qp}ztE&j%}(9S zz0=bE!g;@Ic)iWL2dEeKaEt^)#oQ1@JsXT^{B}7NY($vlb$J8*k*&-(Utxa8Z~2m{Ctrr zEN6YVDRHlURb*;Qe4d_~2dkAA6%!*MT7Z*^TKbg!5>g#v33E48`r_HWq=20=I|Xyo zrV0c@-Dkqw$+$>kC5Y}b*(yq{@b-x7*kaLf3H;*2RXQvFOV~0t1n02p*vn`>$({qE z@jVn2s_>ff)7l{aG};9Y?88bJ{;#M1{zZkvhd&zrk;l&YxsK0#vNH51k8Nj7Is!~> zCH|G#`rnSr{fz_tM`-(RfiCfEULNqYKVU%F{~+<>5^8>v(7Y8Zk(Z>lYbC}D2#b7| zW<7;|vPHz1f4-d8Wp|Q^Z@;$5Qtl~~I%K;<1Jj?L{D?S$1*EIyF-vna2(uSe_^NXA z5(2X$O#gkR3g(5867wwNgRaAv@wxKpizl)cV?9RpN~m}5SZR$_ zCaA&3JH}GlXEe8$bIT5#CbH$Pe%>D=vMewiRxw!a@y=_1_YluO#&nA|f)i0gZpCo- zq$P}Xz-HH0G68-W&*xc79B5&{o89bQH){+9zi@ixt^6_aK$qVuhm_S0wAGxKT#L2Vbs(>KUhe!hxbHnQP*5wgy^RI zgO6Wk)%eY1P4`=vlVF_f0**R1XL-E3K|1^{+Jq4ot2us3;l3whK#JDPuVy(6b!>Ap zL+w7v8!UY;kny^kNue26Hxo__kUJ>+Rn+hDO5-z*Pr;u`T`q5NU{yr)a_D4ju(`09 zvtd{Hnk%mp(He?b!^V$(>p@|Uzl*oBgoLT#ekYs1@i=DTZ!k=b*kR(|iB-8Ud_2f@ z{1ba4bl`{Wkst5uDP|8I;uk7<(zgHpJuVDL@TA`|B-)I&iVugNzW%O`k_ym&e(2?D zOSY~wvZJSoxXl+#bg=%_28_gQj_G900G~SW`Ok9!_(h4;P?%r2v$7yl89d|DP)jHj z>hqQWoM=A%xGU2tz}CyAmHQELoeJ;s+5p2E-sFX@f+&9hwfW2et54x5EP|ecz0$jg zsaxkn!n;HNTT)Eka|7v#b&q5y^0O57^12XOtDYvf zp3G1u5@4b9*$w9Go+I?L=tGmG8murHcL$(dI`)bFu0F)4@Mczuhi%_`*fyi~H* z(bxM1sfRH$j>%So`*HiR(fKwYs5xMZ>e;V|Tno3OJiJjz>EHVtH zd~m2CU2NlOxyF&%$$`<#P$6jom_SzrRdmH^Fw>fWGCUn*Nx1elh1Jea|FiV+_hwkx zs3B0bQ>~~omc_Hqotw1-Ef1coix+sV{cmHr-yO}_i(#_+6(|*3O5WE>u?XY@Cqk}1 zLgD&AxEe@i9i*=T7vY%e_%zy2QJl(dj~xpb6<=aNJ!k?~!A_J`-h*0UR{2ti+|{^l zkswM};WBsP&&(m=GM5*|m$W(F0I89ibfD?EyEY(iZ=M(}PK?|S?;;(PtNmG~S1&o} zYH4KbmCde;a6Z7gmDmg(%^I;0%kIFow^yjN5;d?HWAY)_G`$6j;{s&%X7>?AO= zI(wBX5-n*l%QN&3EhB@J)nE&^4+E+#6BvF6Rb@oin_O}ej&na@r-v9oJKRfd?MY8; ze=5v#4EbU@$t%ET2TxDaUMgSR4NM_lEJi3%LEnzDn8H7?WIuc@Dg_jb!wc_oOiQII zbi}w%SckdcNK& zJg>%i%yQAa&TqEJEJfcS;G%q$Qdl{>t4z5~YD9uYOdPF?w^SQJ5#`)=$_FXwuxc zo02!m(7|dDrw3?6kjdwHLOn}#c_-H>;_%%w#dC3za8Rrbb^i7X>oiuf_{VbY^oOs4 z`%3otCVDc~)TRihJ|lCGg*ej~-=&L+uI6&qht(7zj+tiNZb6Db^}>c4q9|Jj#~&I< zB7n!JAaVM!hs;7UlF*O6C!87HQD-(M>IRfoOqRf{q`;f3?@Xmfp+wD{c49%@wPFiw z1?)5u>-d6y1M`ZNte!t1n~UM88IO0(kZ}1rY;|!H#2>`pQnCG=w1{4mQcJ||JY15% zQC!UCu`~K|^?FM?;_2c>Sh)E-%p{Nkxw=_I#Rj8mt#`3WL7{GLf$#sDf>G(00527X zDh<`-vz%)KTwS(26&DO#(AA*s zeoS>$Pj&aTW$kP4)vKeF6{S!RKOh1C02CQ%@h<=X)Cd3o(E$$&K6BZ&=mP+d0c6BQ z)w~Q&a}oU1_S)V9r+g>J&FZ0y4S}_h+mO*6pLfFLY8J0F9`KZPb!+rb4IC@fxADuH zJv!|j(`|KinewL>$`<3=#YLU_VYJOKFg@TC{WJGKQ}2J|Eu7$!&8P`wyf&2{u}3-r zrZPt!b1f+{Ea34W!-GB={XYi`NHd=QpTELIkRT|46S@EIMSt%kj)0$J=Kv$tgU$+# z4Dt8ilhH#QxXwFpeW*vsH*B10?A#h|U+qwiq%L?lh;`!Q<;~Ig?;1o9p<|^LA<~X) z{%b`dwvcb79;L4OhsYBGao8on)|fqFfL+L+5Q&-4O(A4z!73A5>(EW{1f;!%C~2~* zTX`InLQ%Z(c>gXU*n!MQ+=W|&Q}ur>>bVl_>2?dG?nA+|nRWw|LS?`jeP+oCpy?q5 zR9coTRymc82ks2KcGgyF_x)o@@J@UX#>ntG6*t_c|Ja2btOmb!YxO82yF(9KO>P7W z(4^n-tzublvQTNxpVGOY2628aS$N!gk#8?k^zlb+-5?zyE ztH1WC8)170lq=nM<%!C1hNZ3Xdv3=I)loDc{I%ujBlr%_za=|4 zBC;E0sBOK`1gFO~YMxJ_{c8|?czHd31X2DdWO-I_IqAey%{9wLF5EmPzA+cRcnEP} z?*KmJpqR8NB$fiS7Uox(w9|8&3WzuD5l|o1I#)sF@ZX09OM@R8?4>@D`CkuJfJDMg z4@|}1gfrGFWnU;k1QjlW%Mf~K&b@=T3#?K!4=H1@)6XO&@ya)B?8F5x-@~v&= zYE_7#T&h1vxbDs*9Bco&MCKotcxz7-{MY@tf+P3v{S0a3K5=neVn*h<^xX{jb7P3Q@Ae1y_F)!KoN%$OfTBA;DX+NR zO?{r0%L#+!TJ3>_zvOc5zViIZT8eYAYaNe>Bjv}DjSJlu#$U==qtM7|L-l)!5+4M z7QlE#?%AS@f<|~Lqkh`!YlFF<;w@3~{KK;HDK4sOtcM72PRZS&l+->b?75aKbH}~@ z*Nga0U>uV;rOy%l&AWff08apdxD9c#4}6b=%$G9g-}j^2n@V)iTurXGi{xlK4MSa$}oN`A?LBcT$5kr$#gNu84oN!iC;Y z^2|Ycc7Y>wh`0p5Dv6ri8x?}pt1iZL^{A_R4lAHGWn;x^neSW z_;pZQ%0AhDYGBWm%vOB&InlOD7zvz}BqsD{KmaM?^8MO-8yj5mq3~AwT34A#VubSe zmM%mqkY|hBoZGKg_+Zxv(_i7=a`Pnk4@6(7IN|@W=~)+-gVlX6>#bI@o;SR$SXOHk|GKYkPnq*!F+`h>1It zf9ol@7^R9_0iy|fUdRAS&&XDAw&VI<{%^L!3>9~i`k}Fk1fOLXjG9>a;B?XNUbKEZ}-l0p0aBZZVZlzc@># z_v=lsQ7oxz25fC*g)NEF4jyB9Yf@w&3Ry?4YebJghwws@(6{Ph-U={O+`fTpY_6rH zwcnTHm;MAxkh23nCw`AVmFpk8An7*;9U6p(^z$jV zzPJPoC-n|crXy3P;X{Wav+2Kxo~8jKdX0u9X8HevXlYR8-82%Qq(!V))cUFN{mE~{ z&0fb7-KeAkPcwz}clBL~>3%luB+HAOC!_WL--O6#K?l2xMGvL{?tjrHE#31z-OPDb zun3W}fm--lStW;OAbcklQumA?zLO#W-EzBD1~E>Tsd<@jZn!>f_1DA;ES_)T?2oSj zIxP;w>KYo7lV`U+RV5|VmWR^?e%B*34VxEui;O$p=AZ8iv}gWFroTvx{s%a`1>4q; z)mDT`l%k9Bs?V*Kj`=V#&HibTMe-^0L$bzSo@Vd;JYKH9tw&|9|INd1G_1-J>=WMv zbsq{Ly!&^8a=YM56g#GLNLa4<^}K@Vb4*U0{&^|teL$z#?9 z*5A`HPTRm3FnH1`R0*3E6CM3EpxY3-!TNu*VY(k0myxbJ4qw>XGV}iP$LIQIVeWk+ zG{-@CQmK3|=RtFTFig)ZYz7}}Zi|z_8z0oi78a88VWghH zBg+Gf#==yf$;rzO)uW}&>rkYWrd0Ty2h7HiCnA`Jje~??=jvG>qKp!`+w`hS>g(&B z0z0=XHvh9A5dnzz%}8#&%IG?jHT=3(J<5m(S?L~Hs zT=7LCezPt`(OG;ZW)p^~e_OnU-k2IETCBBOW-k>saBHyqH;aRJ5`x}m&PuI3m_tg& zLMQW!w?EH`&YYoB-49WXwr z$TV%EEGnv!#lRpC&B%xH!7}7f92Sk7+8c5SZOFIi@o~t`a&}U^B8%0xnajApOU2BT znvEUjN@qCv@pl?`UR=$HJj`sg%NlCl@F+n6zHds5&{D0>Acn*5?0&zihu-qX(zG81IWNEhb!!1@Db_=;N_Vt0Y zAOA!nX=@#nblUkEy49oCR%T1;f6Di#byeR$vtngaZ+i~MO0vFw0k z%At>+Yiz2Vg@Z9ZLZTKsN*-I0MSN-lZz|;v-y{n65zc8~9Qmj2Ng(ak;;HOrz2BpY zw=yf+Sf~F%*6sgP)D@~N!*}pAmw{q zHETj-%?#z-BJ;I$=y^*-mT2G5Ex2J}D8PELhH$`BvbAh$;sOe?dP%YVh&e4!*^Pd_ zMNpq47{5^^CxM&R@whb1UPRr5ZUhP7p|udJ3-m*7LK$%e>96!UHdp$)TfekX=Y48W zhT4SuO%Hi2>L#}nytNYBTaAI7g|uypPfI5Gi53%%QPELhybRVWWI+wH47Ibx^`N}u z90WTu>3&~$9d?!)VKS79bbz;(e(G0}_qRFn4-=_hsjE4~GZ7SWv-15`tpASI!;P2IE<9WyO5QGKD%#8k9eS(ua4Cg)s@o2n=r_bm;PpHt9J(# zzrqD5qQLz>8A(Dxww!;v8_huQz|K}}M}y_;c@`|{MVy64mkw@>Czr(ZGh3l%aHyvx z?#IUrms7d6hrjWhtwByyJF*^9L!v@7X7{Q?hl*T&HsyrhA-II}(hUhfmqzPKS|}UB znsbC#gEm5o>|QxU5hktUgv#K#l0`G;3`poPgknYR31EF6K_$J3N#$vW%M!33dK|wo z1y}}qXH<%`G!%u3^kx%ScqIXfZ<1>;A)Lf#)M)k zyOOx%@FL^HVNARe5^GcrR4)F9LuCN}aX&my7T5Q9D(^7U*QL~?x}#;wJd~TC+ZMmN zBzsVdWI4evT)ee0jC zozZqkp&~u`V=X`W#(O3aVH})mqlg#?QLPRE@UhJ)ZA(U5Z~1`zM$|z{gXRwFC|i4n zTn_7o3u(6G3M9%cjmKn*??uq}QLQbbZN($~7aL}1c25M5_86DoY2iRwIrM4*aGiid zNBx2ozjqwZIb*nKvU|;r^5Nuoh99 zWK>NjPg;EQ<1`F%DE!NFB9>o|IUS*0AdYy8lwLlZ|W3{X6lTB(an)wi-6 z{kI9LCDa_Ck7IPF{q}o{K?)v1?9r?~RzJZa{UsDr^~DQIl~W8>pLBYmA*3Hf$Q{;> zR962~n7t>ZXlD9X-uRsl!&|N?+v+GAmmO@*8@YC0GAqEl96jr#z3I4+`5(^roX>ys z(-GBX7r`-%rwb+lrmP2+RFoNB`*XcuVUHRIL>I9MEF+Gde7t zpnlpBNe-#qM;CFl-48o*SEvzc@^McXw00L(=P;!~$bmN!b2|+HTyI{D$gl${Ay|c0 z4g>&>MXhNmOBq-=hSxm|f2QEbxaf=BEhe#B65Z|_%OYcQJ(+`~zJJ$!KV960z~pWq z?OC6BcX(}x|AcNjM=YQSqx`{hLZ{}HF(?~w8uKjra;pWgMIN;f`RuKGFZ0s63V5mf z9+H(u2##9o+3!w*qv{eEqbCji<#1oZ`~m9M{$Fk_?@uqcBaG*U?ZT=NgxtfOB783R zGg#mT%ztD&)r1tXWVOeO*flktRM=cr#bP>+1Lvd|fCe$EiaV!YFjR7uw&zTi<5x6q0WZIC-uaMdD)4&`3FP$+<`*M<7J9 z-8b=E@}aci*F>LLhn6?Gl=DD;Z!_+vdJgK+HQt#l=9knpwqy!g8+bW-1bRQ7&9y6D zm1my+-I{M$gEv&t^aaFKR8&f0`Za-lxnw!V8bw1EZLC}@D7+38{e6m8V^ZV(RrUdH zJTvZJ_)|CExJ2PdiL8d=+tI&fap9AGJ&F+3no3Pgou}bSI+`;?kcX_LYkw3c*AD%i z4H#&rKVt-36nQNf7Eo;n1-y091NW?iAunGK35w0e?h6C)RV}7)3V8Tyr8_*#$gXXDJ27Ef)n-7V&DRT1GzUZREW@5x>mV=JVtS=k4f6 zVPYn`00(#?iJ;7AJWW4{P~Q%&g;rk`TgWuN_`CPZ@GHXA?5FcVoNl99_*d~NP}`5k zMz}67lbQ}}rA+q0^-4IrC5Ky2bS2nBuBs!&7S*;D6){D#7!o*E+#Xr5x9#0DBLZkFl^T~9; zH946~S|Cs*@1;|41I3uwxqCwmM?4t>MQs;U@;q|p@Ar7U*J;51J^KQMLw^-IK2;hQ z&#q1}TKDoq1!7&ZdbSwwhO-_E65*jbCTFf5Gk4C$o(W_Rf|?m`+LqnGtXX8I#=*Px zN)kW`YRxZQ88Fvb`)!U^M~&-;F@YIEi;%G0(2_Qw-L*4VJ%^0Lkd86(!vJDd*c|dr zLf6vf%R7q5`LkMfC>021`h;u)X2@WnanYoyhHa@aBreWk%Q0amJfmSJJ^=}*3 zz}QWQ3jR-3-sXELbQ1ky%m<=D1I0dQbWcMidQE=YlO=9r*&e(M0q9$3ne*)AX%{gv zWN)`+dM6NcRJ=!XnH&X|rjP^4#p5`r>#Yv!d-GVOU7V>Ifuh*XSJiN)FpYms5M-uYmT#kgBI))+d7J>JUOBfvN?e3;$A(B6-x)r4rZP2e}6mH#(X@ zD)K#Zb}bU6yWYwfXW^xBTc#L&#_QKK=nGcJyjIA3N!@Tn#26z{S&?Q5#pLD%zt(Yo z>1*s>G#CCP#h8rJ=`pseK-lgNftZ7#av>lU`CcS+D9}Qm)$?6x6T-qZ_v1895@VtK zTHN4-)*3pMfu{Hpi_Tytbv>ma`h+q=U3DM;-Bm#kAcEO@8r^CeDev-%oyikfik7pEVtFi|SdAX?e{MWmAeP+K(PU z;4o;=s=1jWC4C;J;%as;<;Sz1Xo(yk+%557SEk9y4o)XJ^uC`UBqePo@=u=Hbsn%8Q3Ac{nhkV{^hCkDTi%?m%WDB284OfyQEYFe|53a)px(s_6v&CuU{ zd%(3mbr1#QAM9hSXC*cJ6>mg) z*F-(8;pFyl)_}XkZ2wYAJQmPTmC|V$BrU%-C7FYh&w|5G=TY`gSY-W+s``u^^A@t%5~ti#Q55Q`*UTj+Y(jmRloqKdbPl_NZ9fZU8 z*mF$-KqOLgE15{hyL5AZkN z&!qPQ0Z19z2|~6cBIk<985)c~o1*xlv4NrePt0c?blfMxP&~R&NF%+;wO-7@yJ4&Q zM$-Nz8wgyxK67W$hZaG_IkRWkLRxg21;4CbWH@Ma9M+_Jx@8b(W!|XVQ@;Nu;m`EO ziOfSt>a2fxEpU(8{*eqKS>IstF^u%>1nmlxvpK!>dBaq+`x|oXtI!`*t z*TUDflLG$Pjr#TL*Ig{iu-CoC;{x0Yt%FEX@WI@*a6 zL@{Bl%;-Y}Lu4PmDh@QY^JgD!Be#V09yyqKpGJl)+n8%Rc;IJOKx(<>6HK5&0)!P# z*V;gKsV{|x`^4@CN8@_AWauowWCCVLcXtL1It{rf(@s7`PPy?4t+YMMk?7rr&PI<% z1?&bwmQ%9}@_^0ofdS&>fnN-1TN#I`d812#jjmf>6{lwa%1_#i?U=(1xCV7Lfw&@! zqUl<-!lb!Ml|9=&>nqocu|-2xAglOWdnRDmsw==^_>67ide!FyXrxeS)2F9XUbiDj zN#lB-Eu>FfYb5h{G;jmmgZaFo;tP|XmpVR{ei33|`zEocX?dKR z(|CjA)wau(q)&bvp|QdU=<5&S4a$;`kJC!<;ILy1Udv-uc+D(KN-zd?@DJ8c9gI;G zDzUjTzLaD|ZUnwu4ey0@S?iNc(@400d$VtG)Avzx3Jzf(X^^kCc0ZDswF^aw;&$)n ze^()dFQ^v^9a3&x9dx#>CQqAP{}gV~nsiq8yLk>nrvF%s4|wZis*e6KE(H12Qo~v} z;A&7>27VS6HFz>|H>=gm@CqS+waqeXBIAFss)AZ$n@cL2m3*WmUJV90P1Jk*B%z%)JO3|!XyDFp;`Ys z%E6cxf7JYLH<0K?W#dykxU+sgX?2{sm6l=T*ns&RJJ3-eoH9O!H)fmAq%vDtS{m_7 zxKw12jAT5Pc;NU18@VQSo*1{LSuo;8i`oD(5x&FIG;Q~As1mDW@`VcDRTPxaD?BgK zG9~At;dcajaIJVb z;s`H=ac3>68Jy{Yl_m_7O-oyIP5>+TIJ>3Hjqh!+fN;(nXtZSvAsdH_2;qZ zb42Jg)#@(qULhFUpZ>5?a;T_fjJay)YQOKn?6N?-G&MHP?LtVy+EfVnD^e=u39YAQ zr+$*cSTBHAMXX=vw6~H&+10{2snd#OApBb9fZ9Z0xrRU@gMyKMB5KXNYu=vpyFT8j zR|wnbGQgaJ-X`RWl(!(%eb}wmHYI!Gs?QQ?LPXtI{U@J(`7+7kGlnOZFY7! zBFD-oes>UzMra!Rk3duA$@+^xJk9(=h?S@Nc=38I z3#x()t@u5OAb8z6=$Iy*W0gsqlh9<2OEQE+VahH?MfogldW@ic@|1$#n4v<_KRJNT>mKKq7 zr-K4Twea)B@U}?Tb+MNbESz68Ii zj(%3)Gu@Zj1t+_HF+sPccn9FE-0acvkksMLs2BuqJ%w3WBA;o(KS_qRc5I^IM$A>^U>5$?JsDXJM`U)1K1xGo8?~ae}@r_|@n%7POy`p&!AR zS~^cB$7gNE;TeHXb(T5t3pi{6s+J4KR@=dEf4ZoFM?g8|95ZWYnL^ueFQ(_p-v+Ux zPQ-8FD7>**`{;KIFw@vB@tKZEJjUMoxZ##QiP`&K8IEC@yMKFdhhE-oP!rjC^dSd) z#d+Nt#M6|8EE3v5bVqiulR2d<^paGZ%(u0%DM#dJsCQ>YCyRP?>Cl^6TwIj1hL@S1 zTV%SZG6n8+?=S(Xtwu^8FqLU236L23z6=;TM(%petZHZ%s*T|crhkoIx2UsQ>?2bl zXQ~6lWV`wKx%;+QRZ=^Hq~qI+hKB|_OolU>zcmPY+D65tMQAwNr{M9K{7%PZ0S;Cb zoe=2%M+=~904dEul^@@mJ}P41(pzYsM^mWvrL#G{UB`O$pgSNIIDh%}Ps^Jo1ENjQ&{Yv;8X_ugpiy_1QS zHn@ySx?n}=@*jg^_?Dm`e3X(&)4Lr3Pr2!Q!J(|sqmKN-ehM8>XMdLcOr00dOY1SW zm~qJj**PrVA^JiG>F7fAv@vXJn2HxW!kOhHIRhqhVUVfX!Ie!kKazDS(RW<;UH&rJ7Ebg^cr%P) zP!~)C89HP9>D=cOX=ZwQS=Wz#4Dg#Cx|}-4WJ)Bvyg4H}>}oN$4~p z=B*UWOPCdm3hV}sm55s*qZU637^yx5V0?WQ2KSpL3IYaMW(x7q+iU*>dU$si^;fB) zplYtq3IWevBL}PTc2ts!o?~|#Cin;GSnZ>JvDqTEhx*|~)P4Tr;~@Q4?QOMZofFy) zcaVho9dnx5V~Z9YrFVYnX(Og4HH)H&dlOYf!<=dwP&^l7q$*5Difu&jBXCDfbg(+R zRZ>pU(fgV1Acl%F`E$Bq|hy&(hM z(Vuh#bZsKZZYmdZ)^Iz3AFY}oy`3De)FaeD9x89Q8g-r~Fao+PeBslMsCpxz0 z3~Bc8PZ$yg)$2sfgx^-%ecUG=u4Y};ES~HnZU8+WEr18XcIfFg5z)d1&*i&8k(_l- z4b1`?KUM$CNU_N3-zkxJ4b=OSaZYI1e2k=QH0$2kY8TNs?DTTZ!wxbybX)A-3mX+8 z@|g7*H0W69bW&>=aF*;j_+u|e4%A`6${ z`v^IVaI;o60o72Ku>dqq-Xyz4{u43z(9(ii;;}J7^4<8VYe0JK2OTEKKiI(7`K}r+F_mrOOrZ2d>0*LvU;=qd_aWb^D z^ep7J%@U`FVgAJ`KI~;Pl_bHomB*}w8{(j0bQxT~acjs(|xx>qD#7GVkzHv&(n&H2d29QFEV(X{di~Mz~p1`O0adfNcTr@A2KlC#t$MZmy?s* z3)6|1IQS#;5IsQVSsXRr&Wdy5u8DhcVVay6)afcN6OYkE{+_NobI1|y?c7J^S+tg{ zl{nb>WMsD+ceG0_SK;rM*6)}x7{Gw62BSM zV$l*$g?mz=HM$1a9sFE5Xi}nU*^;(ofw2q>Vx&%_nn|OKkJ3KT-DIgUwE8(E0WYVo zLHFwBSOIZG&O4(uvQO{mLnY_whY;zN?g&;r4VE^ORvrc3>|iyTcEWx3$zC((oOCx~?~P=08)?_cjI+)f=*>U094+@f>j z$%#j2YH*xfI6OLqQ6dPuIJx%C5g0g+og zdu!_-W}OZV?jg3GFKfV`49i~o4tyr>IVDq(yK*d@Mma8r0kHe=GET>Y~94xU631hxTQCFX3afUIK{%QAc`?H z1~aq!^Yva{tJ>|ZwvUlR-S&i2$1JmRhkiP^XF>VEYZEJEzoLDdqB{7+j}H-XND8_( z(l}vQkv5dSfdymg7A4NB=~M!A^@IqjJI3+9>-}ol*9qiII5NDz`5coaTT`TbcJdL9|mX;<=iSaFh}Y={0K@HaDP8Q z8~4@(kFS+C2sqm^qGh-6f}eg)sc{f?Y+DLZiv3BM^Lh^=#-)TC&t5d2i&%V=-Io9N=Fmga~gR=Wj-WRi%6RWTCAahp{GOir_U_|M9i=-=+hKZsGWkm*lt;;}7{jA* zQBY zz6=jofs_;4M8`570@GW**TKy<*Bi8W;{4MQb*O^R;vq*Z$&fmKeD|*K8NR;2ru ztDG}}wXKml{dm>0x%pGw2Fc1E=k5zjUDXmbpKOc|jMVDGqVq$ul3XUs^%N54r^^dv zRl0)K3fA7ut9%j-LMm)1#-BOZ)O%(~Lua!_;UMBFG;2_ABM7(Si?buz`>G!^U>?mp z^pT`cee8Cqd`I(kwY%m1PTHRy1?{}C`b`s7$akl5Uw*XdodA5Z2#jItImE0`wcx|B zUF?h+H8ELfHo*+Kq-E(x?g>e6eR#(+`_KN;i&XiC@O25^mJfItuFI^$7`D47pPUZZ~(d_J3(HhWh z=+4r|nil41+0fDZuL49Zw?T}Sj|1D;_^=QSy7hi0nV?t4XJdZ)G&rt~*(|1drgYs1 z6}&H8{aGz`-Wswy?gt@0_372KujdA!Gd9=>8CFpq?0TtxB!m8)^%A914zQ?~6%;YfW#n*jh7w$8d@_~N zr`>iTrhW}cH}L5s{}JWWoUFxbX=ZVthl2<^iN?w8OC!%$H~59fJ#TV^n8-PA^3rGP zs&y-xn(AcPzF4TkLekY0ojl9d_T*|In}M95VQ^H>{V-^BWW>Ltq(rB(g*=11r0Xle zzXF&k3|a#{X6+YK=bI|r?SJJlt`(#mnRtvp9$?wtTS8M)#u*Yr)k>Pgf?jLcEaW{n zba`dBefv$WP_6A%#RS7p_s)2xMc!KFm;7T(0%sO7v*sA0S_Xw_Gc{?WbQL(Dp9F#p za7VU@AAgVpndnvX2d&VEUz_7iv_fzB!t=bRfH!QOGT zz?;>P8G6qM`RzyRd)ZEqy<8_?6%dDsIv9o0aM{+&tyOcZ|5S7}UM~PFkiS~IE#ZsTU*~6?kafv1*erCL$dZL$@JPW*rNnxl z5&}s7Zz3!)yWiMxvRss3MJcD~+qYum8+(cq0~apcVsCVo^jBk_teSVeJv2%K?dr%h zVF}0$8`wX$9vAv&ydLTQ9EUf4@8w*(&mMU=++l2`bGNNO<(;)RHEV|R&EnkHScI06 z^Otz--!rd~c`>P+N!2tsLWM5>b6PuQ4+9C8=l@6H2g0fCBb1>#ye+~WFz}(mQd*6C z|7K!Wv*VqW-k-G+De6*!HLr(kq@C3DByBiZFO8LOXgIB~rrlxr?X%&V7Ff;xrfwtC z?xi+smwVa&Xv%%R_nO^F0Qng_c*^{$cyTD4aqx5EM!J4r3hZcXR2!AeT!0PkGLk8g zW9hnt8)&5%t74zT&w}OVq$m99wStinDLsQSLJBkkT6_KUu z6LPn|mq4F!n4V?~7w@wCB6~zJEwG0H9tjNQ0_L)l%&YHsqZ%7{SiQh`LR6e2Gfs0w zIIEcNhlV=3Cu5_dz_>8^TUcR6;11kA%A}}+A}nKZe%X7}t`kq}O1sV6!a{y^H7*KD z6MO`5rOyfRY@+sj?{tM*Uiyz_)ZH((8T@Hx%&HzUB|KiUUPxA*el`&bMH7X@R3 zX@u9LzVTSp@{0?7S)u;S74aippNLF@8CFtvJkNAm z8(I3Oyr#t8dic7_FYGmNHK!_7?pRK$K9Y1M!+|9;?|r8W6xhHmMn(^$%KI#(E&&Xi zw?LrHn_5&EOfXxNo~!AvT+z?;Ba0Q>Cx1K!muV>NqkHx%?g$@yzK_G~2O9L}cfNFqEyf6MwNkwcr7Kh< zm-h0fPxGP5%h(nsPmW6ZJGn|#2J3E*u>=%L#lmnU*`l;&;SX+$7_$xIPo-^QQk^Z8 z?h9jPxM!_XD&)(}OtHjNya__qBf4du z$gOJL-JW~_mA$~AGt1OYsZ;$yoV+-B=6>e*XC|2=2q+_zj?^?2wYIjd6yH@*z00Rs zj@(6M-eS`f(!{}ugICo8diWc=l^{_FLJuQQtW9W`WmXHVecAsp`8yK+o0f8dk|78$ zT~9fRb83d5k*eeNxT)>oRt(E&D-rGpmsKDx(fQ_i>J!^)w2`(4rR!aFc;bWf>$1i3 zq~+-7-2{Es6#;eEsz;k5fxa@lqa>_{X3F>G^71Hnw4Q}-{jQhWP%5nUhh70$N;G0% z+&HpKY))&Gbi7tZLP2U9?{w_S3a*^9zdut}c5%Dpa8(;GdL1>PoXc>fJQ`T%cSsD0 zl$H`0%=3lsv`li~-FIs3^N(U%9W;zB;hy<_V6=s&Q~-DR)%@7yg0w&dc^5d%=Zu(LIcAU($VJpf zc#=5T+B6brTJb*B52#rUQA#DMsSt@V9b|8t<1EWn4YFdHX037Ft5{ffHBl16JlE6F$Ig(g`_53H3Xz)+@C)pT8sbqjCyCW<}%Q5n)Lf9nED>x${$mkCxS}>YDOBi zMzbj3^p3|DO5uhqWRhb0S*-dJSjlo)bxP%zYp@TOkdn|?73uph6X)zd^72(8_uCpW zC$vvg(A2>n2rm%Qn7frkTARPn<};wg!#KA&JnMA9Umv`W(etUO9!5Yvjlp1+7D1jS{V@& z2yLY7_sE2Y``WWX&D6CLNMp~k)E4-@ph5w=DH^*P5s#ng@#8|6wcSbJo9-R$Lf0P; zkhH-plnf{yeZ-+9Zid2SPha&N-y=P>t)QR_+*jcYZY!?q+xHJmEP=cmFhMeS?KFqergS7c_^aj$BP?gd}p& zFr=e<*i>k+f4nVxJ{FF^2MY`psrYZ2qh;cK}C#hLU}bb$CR32npGCmV10%zz2UYbhHr_3lM5M& z$KIQM@DbUa0`on)Os0=1LU%iC)%KSXv`a|86}+4ab5A|~jwIK1Y_hG>`AtZ~P69t9 znH(>w@pHACz*SAs(u1=GWmS<@imP4poJtxsLZ@9SkAe1`DgiVqH2wfB%qh&+*yw_x z&kT$Z9m1YV8(5ga+&#GN;|p_KZc90K0Of(-?dvfZ4b@;dr4fOuzRRv_-JGlL|MobR z{i0tH<`pWP=AuLqfp(n6z$iLzk`&)+&cM)DUPW!vs`xgyAjvprOf$#iE6o`-Kvna2 zDMkVGjjhdG0*f+%RLY>%?GwN|y=b7Rgd(m9_kHrxW_eObJ-ge`AeoLK@qn!OH(RmIVm8a z4NB}?ME1Z4_i@tl8p2H&z~VQ-u?Te^Zb}kQ33rR(iRW_uK_Xa>@I9PMtZBz!eIZza zR+Hp9-%m<%fShSX3))4VGN$%kCzKIy%SJ*S+SeSwgB@+Q@AH|zaVt#IP~UfiDjm6j z#|=ts6LKFxgtFL;3t|fJ;H|jv^S;y`jC;k0wI_|>p-`pL{l0D9v+*7Z4(<1?y5o&hl6;1sE{>W;`zdwJ z>dBWhkKN3PMm6|WfsgChzv7dL5gokma?rQZLH^+8csR7@>);*>mU4>GRVkQyd|72% zPSL($g$VF^s8w$8Z(Fbt!kDxmBMy?Q9COa#4E})cgD0{YRhJ$NRpccsvu=`)kp)%B zdq^8&XN6a{H!pi2Ezib*qB_u@$rc^N2K}Koh@Y=ppfeO92{F|d=MoxAaM!`Zv7oFt zgOo^+x@i6kS>9;h(Tn%H7zX~$^93YsZND>Q2PbNcW$Lu#cwfJUBD9cTmSr5fppp?HrGeG z#Q}6j^ZhcRLuu;9=1(+-n={x|N{inhboVix1%ARHJGW@1nN~3+F*?j?;^M?1sCZFh zOd5r!KRZ3eS=2F|fWx%)x6^S~U^39Js}GDa>o!!-ZlAnE{xOB1vV#0o zNUniI!OS}zW$+DDUJaLGhG>A}a#KFOuRl?tX?TWTx0Y99{l3-_cog5ucPedC_&orI zZ8jZua_W8a&AUFh;Y9lGq!&vc#eimUp_e0SjtG_+#uOjGe_nLmp=r=M`1TVk8didF z$|V?$ziVXB;gROix(FEdq95E=96NvknR5c+gBM$xpIa(`>tQr~4%nCO!PC0K!)LRA zr0YrwmJ*oQh|cgjXVL8K_VWBD$!h%42+4dk#o+a$J7@587CjOpGTJ;!-QjGMvG9TA zNAyms_g?Yw*t+;r4S3K8=B_rP!WWfebr2q6kb5V_uvVs0wP{0vi9JkRJz+D(<3HEG zTW8xrXuJ=sElAaBh@>q|PG+Yk^M^q^B`3R?%?0rq@#9K8lsI`TCdfn|$x^ zNmCof`#bFxwX0WnXoB@azN)!X;CTj?Jy@~Tou)5It-dyRoGrD1_$1;i6il4@4(jV{ zCr~lCyGK&Izh1>YE(ss$S?MBM>#shsCdA1QSUXW=`c*KMVPI8z^yn`3c%M%xyTg%F z71rwqNnwBx(eE$*kz}-iKPR)(4RfFmcjmOp zyD%Xy2*Gtx1%cQLUOqdtN)$dNB#%92){65~;CY$h4YD?c$Yxz5eVl=(K`uGFS5Q)< zvc=u|JDj(kK*};n1$IK}fDU2?V7rcN)Lar5;)s!jGjm-9sqS zANEoYU@~$nTXF&hABtPr$hR6>S&n7(m2=Nf=}GsdIkGf%1=0OTX6C*aPl&6^8RbaT#yg<$p|FcWxExA-=F&Z4yo9P(zEbVAVC>K!pxPVY z6Ki^Ye&;!DS~T!Q;fP^qdP&-6VPbl{s6O2|_!G7IXMKj3GAW&Mj^5FBk{NL?TUJhv zY9zy22J=vrTpMUK5>EMB0|Xg))Oi2C!>p!=ndE2Y$~8~!dx6_ZSTU2rkJ!{dB-UZ_ z$P$0b5LHxSR%~4Pe*hyv+`iYHoGm&O0Poryai>P+Akf&!pp2ZO_Q2adg!A{G0ee4g zG4x4fQp^9~mLMXj*Bq$;^-1Ecwk(3Piq1Qc$&v&Bm(RH3DqKD9MhqJzo?R_|SsUMf z9}oZf=h$6QF8{f}6UJ{-bh$o4B##}H2irdSg+VYFL`B6mlvk8TL~~{3{)D$xR#rB~ zj~|cmg%eR&I38(fN4vutk(|^gGLoe0l-+^T*`nV8IE6S8tF{5qe*j#s!5!L10VinL zE+dkYLJBzH5GjY!*|Ns4QWt5)mx*``7y%H~A383q)c(MPU7gz4bX2r4gJ9@Kz%ikmH{d}1?s}jfbvEk zF>y>JtNmLrery9ynOqymhF!7Ci(PxYVBKQ+OGYGLRa7)0MB)DdaP5>c&ZIg2`Q^iw zWKqQ{pL+(SkNia}$x>75mXLNm+7(N3;ke%A~~t=7b++!@?`4VTfw+L;$qFH)*3ei!-3bfy!mWxQnQ#&Z4po> zpEo;~UBTH3_!&TH3^V}xhQRL7^INt&+b`;in#C6rdf$kyTe{}mPTI&xJ%@$-D{G<8#ovq-}JsUK$9M=S* zVpNi4NeQO*nmi1Ph!!hwhhjM@x~2AUM|Fxt)Qa^tp=H_U^GPx<5!r2tWf8|Brhl2t zzK-jQ-NxaQP>MZ@?Fe^kDbsZJv?^SB;eL!6*@Sd&7%#m(6i>Y{5(f@>juOel8sk?k z!kG7;gS)A=#av53bM{btbm>=cuy7i*B~=?*T9$2LT8=mJ{~fk)2)yxaIFJ7VT($4; z+{nm*I{zA2mwzEzv8iRIm`JuNx1i{pN_>7!1sWP{?An`#7hfBOH{Kf@skba5*$Jcg z#T6^$7ehj8pd|7doaOxv2;a!a${u;%C700|7tBCbP7e0&+<}*#D#ey{>o^n&tW}_! zvxkgW-IbW+m`IK#?mG3oXlQJJ5y>6iS#5Th zJ$tsbekXFs9r`O6R{`7v&|jMafjhR0%WN=lvz=!f8!3!8SBA!hHKa!ErBRoUMU8IP zcelQvL%mlQFPUALK88RK z68QGg9p0?4lhDmvkvhvicicNYxV)L#5NMM?3Ie(H9Hm4gZqu!WZ9&vkTZC;Qi*#%r zc@7a_E7`E$PbM3-&*u}dEkZsf=0&K>SYtWv;yr%MMe^k{W}KyME)mz_aibb>{k1!A z{`7-zyLF^?!V}9!Vc9dIQF-uak?d-&!_>#`Mefe^Ac@Y6H6m<3f$70(K*$XcxQ1SAYS4uskHeF^fFFJ1`lXVY+uF1;D$rv(x1UzYJ zsH>^QfxUYqX;%#t?g}~fqO!8G4&Pk0x=)U9w{9(xCpb@87=< z)iu>PVbbKIZcAfH?)Z}`3A^?qJx^)-Wk3nUY6bQAT9v)LyzDP=F|$mwmo8Hqng=qHV6fS1Wi~zgjY*OLmEmV~UiK;vvU9w;Ub~d8tG>ynx?N@}Z)ld&;c3ky#lk z?De|StcK=ree>!sohg~0kBb(G`L^t@fa!KHJWTi?q@1fZl|3bHu%;OT9TJc-PLAVT zK^j+pFbhD7J5J}@$0hR$5@q@A{ETL#AMP)EZRPrt<#$~rLI(au{Gsf&$Dhr?HfGM#l>o{e(__OrTh z*oQe9`qO}QHV6kTWj(cG&(ilLu6Ft#-f^Xc?F*UcQjp3ZhnHt-?~BiVePU1qa$wA` z0t7>`bM@a{&G+fDAzIgbU}deep>Nx;L@j$HiUH zxkLIN*)dTorx=8zV@f(VWn>fsFD#r_a9J0oFhdQ2eo3Hsarp`W7q;F!6Mb{x+A2Fc?$h5w z<%DyfZ5IZ+?1&XP-IgjaNw#nhdfjW#OYQ>)B~&r`?3|trb>6?jy5eRat_)IJB3V}3 zxyKu^B#YZMfVW7qfB7q~J=f6#xd`P$hxX(v#yOJmofz4guuBoY-#tR8+Zh| zsUC4MV8nXtOC(gILS3?6$E?{RX0Pq4SdNNgNf;(swWXC4k)N5LFmcOAU$Ue%V=`B} z6Mg84OFnh3)=minmrj{fi_g!lz|@m!lCmVrdfw_i828Q-7`b5u?4~*dG6!Mj>7T}~ zsh1<<&4$ZTn3|K0Joo=+?>yk6sKzUNhl$`CsW_M|Ic|d6DE^MW|B;jNqF~T zK9S75_uYHmo0<20=bYcscmT3fLR+zb7c+1UUR?(MU3Fz; zW%J*UFD}k34a4^}KnVJ|}YU^gBbjsdBChk{OW8RY>;tZFE`nTeLEJ7&y;h zU^lk7_FyBHV+^k@nVpI)CDFXs4QJrbFImvzkE)SW3$(Qb($mw)0o7RE za3GdMfIQI?Or3E2P6MJ|lv+tbUX=6PKd&n!Cj%t(v^3ARo}~kUV43 z*ugT#GBDg|43RKnd_K}=^}fQh7DK1*`NB% z?rZNXkbKSGzl_jm!yX0rLjauakbVMF>~T?FGJ!MUJut-3jXei10LNpI_K+1- zA?7P&8-EUrjR!*rIF~uto1)G38=b*&_@CmAra1fAOxQ>cFl{o$Ye8Kze#Q)jIEc_bTw);mN}fscQUYK4xHh#{12ROdtaW{&D6v>)b6SUE;|c6xgzU14)%u@Zrl!r zuORa+j0sy(lPr*)o=&;J1eC4Ws0MN^usS`w^pLcv-fKlkyP;6& zIdkUBb}DKU_%~&4@_bEFLD_4Zacf=|_2#Zy0*b`C6ai@tgI-_xZ}}+DD`8E88nO?+kJ6WK8bo! z$)kzyWb18~s)6K%mL!~KmdfmXzZ-qo+|a9#+=4f9Cm#Cv%2VA$t|1S?2tT)Zf^n?p zBDU374ZBoNmY-HryQvm%v%sR2*-tQvCJbcbsNoEsi86mFV_}ak139%iSLe1sdU`sQ zO`6b(yUfYV%&e`C+=5b5-j+z7WddAZb)Eg$9?jF^a?eu^Do=8R2G9#Yg&nXs<%8m5 zEeH*fP*|czrOgPNJ`|Ex4_VYHqFjw43p+>t>gZYd7afnZ+4z1o4SEgCmc}3ax&yPPsh!0J()v3SAIqSdVm^L*=$4rBIOFMQ# zz8u1=UDvh>$*uJ+Z*<;xXPKlNihO976*O|tC*MCaoy)@e^x(*kwPgc5W9fn(|MjD< z+NA?q;9u7aSvWN%uBgYyTS;hBZLMH|#VfNO24^Iq*gB2jOU~KL%NC@rYMle9vvwv6 zq^GB6HINS+sBKc1bghzgdU`r}lu4i~7~6j{Gc#QRU%|M(R$o(4L2|T~Ay(&brMXmDxdaWL!Myuo#VP`ZP#`=8(C8dvB^zL;9|B^>Z*I3cLa^QZ z*C8PiMhq%NT9+DZ+?9x;Vm-n&K%#*uD&56apw!0eK=Q0{OyReVp$RlACW%gaMT=O9gO-6xy9fy92;`07d}_9?uTvZ1MYN7lZS~eyCQT z2l!J$a@yN!D5uYBsKa)c)w{R;miW-00r)jz=rbiLj`nhZ0nc^+XzPd0fVU32zhGPV zqLn$nW}N@64c2uAV6O<_+P5=qOmUP@QJZQ34-2HHrw7_8D#XQF5Fca3ksl)Qw|9C2k^}e?^bA&+_ER5{XHFS6 zNS5WL9BlTrlc!?(E%Q*EpNCIZE=SI(6Ef%gX{P0`Zr;3?>bC~6)q!M#UWeqw)_ouN z3&{nN1`;K6Q=GY}5`x7Hog_iWG;j~Mb~gr{8%GW^L9xFy@VSCE=Q<2dK7AXn~CW{lmIOYJVpK4~PuOt}Qwt+4avK0scZ! zT={=TzIo8r3C+;vTUxv#=T`u4%X-N90u%1fnBRR}n{QSh*b+{m+Ga;_xempp^rJ^rwFbrzJ5syV;EEoVNQkv6|Mn23RzTTm6BG!~ zIv0wQS)nMe)I#UXiH?i0A|}d;NWBPcVQ*+E5}_3VjYftxsuY?Y-$4^zftnf-pKMLW zJF8P+5FCIUsf7Z`g%+U1(~~@TbWb~kr@(Q0p8)$uy?A_#iKan@? zE$4XpaG~#;pb*n><{y(#O0UND$2r?9TIJg^e$wNTgnu&(q7DgO3p{^S|D*_w_ypr@ zp+;blo-6f$WY^2N;QVEyMt(hE^^U6G-CgjnY|)DBUoeJ8!P$@C{1nx#2T;KnuLNTi z0!O*b5Xog0&lp$IHW&~u14)eIYA`e_4327(TWy+$p1r$kaqwG7U13{Tz*&B&HLDDG zLNbLilcIGnw>lzY!MxDcSXkqC zfyhFj*Fng-8rUVK{Mc*)YRnX_%c0lG3h)vfH?(1w1DeK}ErJ5g5n(oHwH#VehN$5X zt)al_;Xrvjg-ttxzC+7_9*4mr3Y9!$s)xMiKrF&EK#GAWImrz{^4z)owPl4dzXHSW zwK`pbF)WNpb~{Yf)m)OK!!pP3W8#yyuU@@6(6wvpL2`I#2;yR0Wpx98sWk`oq@MP<1_k#`I5bWX_qJYUhdQ3}zUInvu(cW)e@cuW zn0w-_yA16F$#h{~2Ve@Il;@({+%Eu-N2A1PSo+YSLgQxvP#D%iC#+Hk^XC9wa|2Df zVVtWdDm!!mX&gZxeK=A-ik5J1^d`9h0KqFor_h`$0jvfiEf=K=iQSx%#;QxJ>RN;ko-Mj~0HBuXJ3q{%?`pqmC)m^Sqr24FM* z${D70ky2+@r>Nmm9w1NlBS^o+nO{tgNmc+L@3`{ek@4R7uCid??IZ7uGlpl`iPS|I z8O-^pr9)l%>SBFZ;Q4E=>=Q5Q7TGzUD==ngsCxsD?CM?y&iW)j<4NAiNY18~}Chq7f_CcE!o8Fa?;K)i@9vW5sWNdK%r5 zo#`vG2vkM`h1~&5D4;U~(HDW3JV;!^m*2+V<+uBwx=Mp+9Yd1dfo=^!GTpvUo;<3H z$OWnq_)~x-IPU53gTRGns%_G`J$v@FhR97lNT%aYs0d4e9rgm-o|t~y%}I1S*_;H zZ55X73zA8hdhiQJj1i$sC+WmyUfBMhv(D7BT4%RhVK`x4J$J4=c*b4Ec7Wti0DA!p z_F`9hj}M@aE&$Z;YbOkPI|ctH4Ff0e^#kxjBj@Qp0B<{ACkUk*L8a85M2wAuFF_ZY>tTEH639zjF)6_FU7o0KRhmae))o8`|nf00*5n z$MhhZ!CC5NDEq_eyw!wwnzKVg49*lvP$8LJAy zBwO_jj4_IYp$BVzH4eT`0>A&61wHfq8lZOSlon_M5iL*Au3*kbu06+@nVFaE{bqY_ zyiN%y_i$@EJC$}I-@;+x95>I*Res`PEC9i(q#^8HtR{2I z+{fiJ001BWNkllsrBm*l4wqAhzNLvWCCOY<<@9{f@IIN zRaoq(vD#5#wku)TB&a6WR6?{`pqDwcjf#IYT$9zy9wPw082a|}VFul08U zKu_bz!%ILRok9D2aLXT;o~b_^x;81AxrY?r|#@|;_`cE zTwXg?A=&@)z3qW3#*|dc6y;H;xkIws3uT<)KkemRcC900dDc+Q*dyGbui#JljsgMl zw2TEkkN9sHwP$CtKze$5YkPL<so_f{p1t| z^sT6uQ|x{fSgZmbS~L`AbBzdL!0-?OI?p8JMlKsd?F%-JlNLvKcK4JTyt?d=QnR_C zMqvK^j}iF$JH3^V>quv)waeG%DcmL4v3TR|-ACQ$8!=*pCN|cltE>!_yLRnrUA=m> z>ws+r$+Vu<3cGGCNOrfORpO|$*idP=>7WJ?VMa}DGg6AVm*UQZ6S`i(@vNc77k z0Oo$lv{hKP3dw=`Ja2rSc7kLV=#2+Jp}wwmGnIR50FvFksq|S*&`AL50!!d{RV+nT2tL#cYae2}S^yiSL*y(X5kp8vX1IDRL5dO08 zv3C=D3>-^g+Flz6KC_t2w?BVxzt)J}X`72%J&NBJP$AiGQ(^1R``uVtBh?eSY5YL~qJj8$1lvc!G?t~^NdjB@cbLK$a#=Cb)cTv06k8%gci;VsYx zHIk|Bu#%RLo}NzMuk^j#5!z2OGc)M{!e!Tj8j|U3lvp_mtt1xBaqD##kUk?9u~C#h z=e@}?@W@kxar{&`bYS>vvIYsHcNGfBF#p&eeT$?QEW zn>@{5?Ai(n%N7Lok=fkxuxu5Q19!bOHd8x6vTI3{M@%>IumdwoKktSEG*-y2+!ENCL@3XIM6QZYdPmT|CPhe0v_p-JHJGPkmib>kh!~ zkW3-s6jI*JEzI2g7_Pi9*Lc@h&+<{OhXe%4l&4G&09@W_@~l%?8S*TjT)ZNibOy*f z?zM4XhnSo?t+SMN8uaMK*#bwp?05*$=1l;yWrob7FU}0wkaZQ3>t7c8e$oF9nX$Wv zrWh`OvrMbqzLVg#1(57INEyI0IfTE3J-+`?n_rNmMX)~C$g6Ds;GFxa`)UGXH!hu@ zy0tylsUEZaEYRAJ>>jq9o}NzMSs%CGZku%E%T~P0Uv_MgHJi}6(Qs>;hazZmEdZ%q zM@881o1dP-RYOam)p+;pl2`Y?t5dP|^AsiA`nRznCWnd8`MFao2ewwp_*;=3RQq`L zv|QXhH>W}GwaqSI|DkBS^6%a#uh1e=%aEcc_2o-eAn~z$3s6Jk#$bh#^Tz1r*N2yw z>MG-`EaO$Pgv>9#_`>BWYJ!Qqc5M`rX+&hW5i!x;%V9O=;B5`bo@=Z!TTxhEg|ip) z5ubL&AyE`1a5+n3s<8<*6)B?GOyJ>FC6g4g37YkKM~!axRvVW6^*)&~mR(Sg*Qi=< zW42Qvxj9!>)5o+EBonX_e3GD*x8@1S$DN_p?jLjwL9(Ye))SJ+qfBAhNcJorYe-K|r|+VuRyyM0%*@Q%&*e7Ae-7j})@EjA()aRZ*Zf4sRAY_IS7Jx zrY$McDMDQzZs-CgGsNnEc)g=I6g6@j)0zt`9AaoV!g~yWF(nNU{4=|3F`?}H127d_ zpcU?yW%;44JNME}qMwl6;Cs(6NT$&w=9QcfQ%4E9Iq&H9hGeVF23i9_a$G`Uo*E5umrjc{)sk3Ul`2@x^hH|71(lt18hiF!ipEkQxj9!w`;2J^ zNOo7Oc8PAeHtz|PE-!Fvc#=tZnx164=iJiTNT`OwsE0W7lRY81DeI?w`2)$6mrTN3 z1j+7-+V0`sl-o?9)c!&;^&bb|1ri&}SoOV5wuc@&_T?wN*8Ke;^c_AGxBc!7Fr{Ri z(h4MIANlnB-lQt-vu(W9;bmZ)zcAy&wSVwApba|={q6QBS+r1Yd24w=TDqGuLDKg) z^va}w<)t3De}uO9jP{`ti}hi>D0CYhQ(NM=fP;Mr@Gl{-eR@mC*D34Q*L$ko7M|qe zH;oC;GD(xHvVE-CCSM}~Lriu_StzYHTBoH4WzQGImAyOi&4ad<8rj$QuYNbAuZ~N% z_Kh`sFNm7MRJO?zk|PZwM%VQuw-zM3FSpIT%lE%=K${d1zZ#r-Q~Y@a3+CN$gw6!nI=a(m$y4xu1+RRZY0UD9x zj`Xv*eo7u9!@Ym6)R;s({c1lP`aTMhB%pg#B<>zL5Z81|hDZsPwik}Tt^a~JX7 z14nVTtOBAaW9Wb~JpRye7(=|5C@!zm;?u23Sf1HkDKix=I_e^8-NLNhbCIJ5k4Ea8 z`(R9_6fytIPQK?08i4%Q;rDt8jbAN)zO}i zOm?(dDSC=~g=F^)*kmqMT4i%Nd6}(}>nThjvzm*w)s<0Vb)}Nm=nayyPoMmuvZ9=B z{F~KOPja)&*5pC$49UF#xIA5?F1^7is71Al-*Qv84kRR3IdgT%Q~YNDe{uT^dEBW) zO=4n`o%za*L9(y))A{fP$@HL)RK1^c)^eu)E;$Bzf03d#sc@4rH`Pn_7m_K=nv}g2 zTP|La{XEra&iUah{CC-Ho{(J0!A31xkovv97tqFA(`!F>Ip=?w_2JrjZfSq#3Ut50 zk8JenmAE1$9N{5aRGMr!k?VLiGO`yr@PXV?6LL#P_}qJ&G#~+2bP31y?+R+iQqP-4 zrXVFIL>as8fTQF!&FO1CFuvr-i?iswx46AGVc0Cm&fl|hu52b+r&XY{k#-p1dpYI8 zGtHMhKHPk~H9W~*-7t1YrPV&6!e$#|w)2rTS?+149564<3CXTk@*wbA($;LFGHU^E zA1v&1i$=h&Imh^}QCbX2a($Qc>=T6+?U`Z=X(TAk8zh&RB<#zfkCD2qg`Q+@`)pJ1 zaz+Lhc;B3Vais)jV~yRkbU`XTuv1$nwLp4$I_Y(gXk=?O&l<@cqWqJ~7%C*U)@9-2 zlV|Gq@dE{sFXP~|Jxf4&4mHahxhBf(g_4te@BC~`8&`l(qrDz>($`X1xem|0-WNxH zh=e3Dh)h7AxEKsgNkBrlLzt?dvKohS3vjZi1QM4aiX4NlEXN-o_z_W&R1i0U`o!;{^xfh4OLIUoNEMPF`) zWHA*2Z2srlcW-NJNOrBzpw}TbI=or6l-m=M?G$P)Q`EX|NOnIPGUvS7&MlP|*<5C^ zTPQ3$TY1BD&XmoSma(@s_C2R!z3BY-W=d|eXmq8uRi z{9!5)MP;5mg}Xd+l0DU}$pbnS0M!ttMq(kZn#WXriJH487Qy33VYNHszBkw?B4$ z{@G(}@A}u3NpAVo|6#x=GBK%i-qPvf=Cr@X$UR{d@?2(S;FE>XrW-<^Z!*B%GW*RM+i zj$f!y{=V(I0zCYmZ*lm=8I8{8JV^GDmu5CxpSlTe?zgTT@&pr<@?uK!1Cr~`&RFT! zg{5Mp-5%lVu>RCZ&aE*OpRKlpR7-ZhpCo%gG8kv~r+&PJ%9#bcy}z*cAq`{n{y^7A zEe0iDTIT6u3w|uTBpKR<;nu$P*=FA5Cs*XW%)x8Dd|nn-YSY-Rcy?YNdT6IM)dDTH zKr4H#Tf9Cm6Aa7D%nUkhiPqSt`<(u{G+S>KetVK#OE3{An*oyqP#t4Zyq-NhR}uB{ zZ1ez^NHob|jw-9rDs!#*tP8AG0U=HgGX3_} zN^L-%nZr&sot6KZW^yHyndpx5eXZ@64O(I`3ALaGa{G_lw#Rx%%v|#^dE?u)4Sr;!S9hfgl4)^c7j4Dlfr)r-!BJFmntwk`znVs zkpP$=*#*XbnB7l-Yl36~>=)J^$9ubT@XAkyfxOK$?-$Fa_yfr}^3u$veE~LL)wf0s zS{ zD}H+Ym6729KLoJs4adtC0*u~o9SM?Mrck`gvdsP)zUV-!<+U$fk?o4A@p=k4XPcMZ zkvgM==SLl`T7W{WDJ>>YLcDEd(aW+{azMVYT5yJ%49Rq?YC9=jb66-6ngLf0F2#NG zv(YWNM#&%cgj<5;vlon5_jxkDI1r19D($6!Wd=lq*)VowF>ac65#77gcpXo9&QyDu zyvO^GL@VR<1ju@ZP&c72Pg_u4a-oGh%5L{C{?u*06rx1~_j1l3-Lhj3l}%~5V6*=x z!Yd@_=jUSk)=v-{n}DR`E=Wr1in#bh=Z$6E$teUoGCWiX!KVNB)!GY^NwABKO-+Sl zUuWs^c)gtF6DHMG{<~uE}bLNtgfR$%uxImv2av_m7k7$ufLD54uZ=^tL zqBl1ExYpE^^;2KkC;hIk_)~XRl&4L{qekB47Uiw};`uGNF;RG*2?A_p66#BVA=DX) z%W*cZs;KvyKOgCvjyx>yg5+LPrXugiVN@47oJzsqQEm*9X^rIdz4x{KwIEC((*((r z1|}$B(!YJj;SDA*ChsyqZ>7nO=hyyFC#0I7o8}>?esFqkJTSeN@^AWkW1eZ3DLV=z z+hq=`ETh_PN3>3dr5r8DYT6s zxlXt>;OR+j=WuIZpLa__A3qR~kbQ3lTD&~#K`=%UI}O~_uwO4-koteV7OB3jTA(ot zv_WWhYn*_~vR1Nhx@iqlUz)R7kW9l(GI?knWtE5`6XGmbIOi-z4KGHhQG)0pwxu8{ z2MBg^^FvTn>iDvYi?$*q(c}>4ayWbIG)ZQtt`V{Cn^?T}ad#9H>6JbdVx43F41NHj zqXZOJ!o=yuO%oacjj{3aO16qp*v-{o;4)-(+vXj+KMD{a)BYNSO=|Ap)>mAA1LCjfheON%hpK$XbB^`~ zWnYj?Yb1{`LGil%7x4Sp{S>IBcE`9>-22+siuc$Bz>^0i;DPDAm5^(KhmSk}2X$mX)7syM!`}4GrZH>qVg!m|ftk6&QjAlGohT=UZ?``LCJ%B5Utw>OgYG z3Agt49&J@frUNcsk@F0|8`2vv%aT0wsXKd~XwnCW+F7-Ln*};3Bwv=bk~=IU)AeRo zytEu;HiwcleIxWrt;U?07jX5E5*R~d1(q4B?_F-_9ZL>e&KU@fYfJ*ZJ`|&&khr0@_SMn_Y1cMq_nwfPJY*>B77Gr&_ij*tvM)$>0Wv`^Wbv#u1H8ohTfnDZrvXUwed0|_4k_+;4vDG8o+8^7y_qpooap>KrzXHhNp&?4nGI^O>6Ozee zO!LzZfp9}jh2*B5&Cc0hh2+k8UjMw}Oriz?)g-F_Db4Wq()np5aMrBW5G2!N1j>h3 zEJx)(YS2)*_N$rK_@V7&eNg4+CJR}L`=a}flSr+DrD3(6im)vaCQmfb!`DFH=l z+2oaWsb0iS*HDPDNRA!2)F?*!zOSx^owdc=h_cvuC3 z35uOC?4EP%Ypp6I`?ApG@wy7ho%1{|UYWfV9E-f|PLRYt8FzMTc4dIZAeoLQS*vU`nXdX1E>Q=&r<5uyPB_HW+# zR;k78-WsZz+|eMJ_C%1}8cl`dW?8k(AemOTctzGafGKt3JevSMCu2d6%x3LE9dub+ zAU!>u1U&1N>pazFF6KAX&MIl z3(^!ZqvkeWkgR;hwIe7u1j)Yk>naiDk21Bdx%_!uh2%?CQX$!&E2dtbHihKJmghw4 z9AVc~POolb*t2wg+Sq38LLH=9K(#=Jus~BBqOG!_>Ep+D;~Kt{f!}2mAjDBgCD9>} z)mS6%aTN(-IZHu_8ZE-Y!Z2*aNQ@gl5s8UOSoHWGP*Pls>ErTn!`SmEsnDUMOoz%E z4QzHtVPmaELb%b6m?$e^BCXi8F9n}&PeDvf3>GhW8YLwq*u859zCCyVRaI4x>^7zJ zQ<9z(u9@Oh_O;GRJ5XTZFw1ZVcctHlP|8VmYvk>Y(K;YGL;&`$U4d^t?HrzDcp`v8F0$zsgZcQW4I3;zCr~^1WdRkE{W4RB>h#3>*;fj( zQd`TFSD^g-d1ZK8g8C2Y9X980cl%V@ z?278O>vv0Sr$TbGEZC-yOe=Jjh5D!0PZq|nT{gc5)wNffY5~;(ozMd94#^oA8N!|& zTYd#blKU)yBA#P(OaVqMd3Y)8*<+QIb6jq7)E=hNf5y;I^y}9jH{LV{ef#xOyvb#y zrFiTQ52LiW7_%nj;f@=!AaIEaD%Fa6xz49j3{(JQCSvvGRIJ^cg6Qa2EPduVBqSs% zUgRH-|A6)DGI9L)G1OF7D|yrLI)+GXeV`|Rr;3C&*`aJ*X5%nPj^jr58;<13w;&|3 zYn@VjG+(mbk&jI6yKSH1*v^eGRh9>py0x$OfVXgK<>pa^WM8I(3dufA6p#)oBnQNd zK{d&umD!Inju*TwN@DizPtWg7^_QF7{DovXfNZzp@QU}^6qHp+)`Fn3Dea=kj8dx| zW=Vosl3{8<@Nr5~oY!exuuS7Gnk*=_Hm&rw3dv15TeW@L9FjBE=Njb-Yatk@#(dqz zuzKnIw0X_ii8@HNfNFuDw?Ml?GFfQWzz`7;iK)|PVAgdvA|f&p%$b0kpLY?9AAby`rNx*zF%J*kkwwzSFjb)r zD7yeTgwkLI5pSqh=1r)mD2E7+Xe~hA zZ`ayLK&(avD(#NC$n@{}AFO_u-6cT9VdIS7zGeHKkAtBEl&>pVh2-)pec7m?*HuVv zuH~9GE|8GC{>B@PI=y(W48%5V6>9H5S|G3IxO?pS&{WSkH8#`56Z-;|X51-1=Avis)MI>}AEV4L5)%^{hVx_Ei^ zt6=!0mu1TWXMHm6NIld1EvX|_3#b+dRtxw+Yi+W0Hf`eQA)E=nV&LNeMrtDwj6u&i z1o9|{i6oxIK>5ApcCIK`$Hm9vmRoPb=rQ9IZ?b#KyO@h7pLh(VB_$YtO%ZiaeW|aQSK_RjgB!CJa%Jl$1$3)PN(iBRsuLNe1DCA30=3$)8 z+q8Z6M=B(@>{?WFl3QlK&N4cXki2p3>;>R#d1tYWx)0wL=$}3Z3H=B7?5Ke92k)aS zC)@Q=pD|-i<`SO+o6(^)Az8LtU@gysWUBB9#tki9V~BtzG#ba>x+fs_9B+*pvY0W{ z+A><#bWgGh$&Ryiy4Zpr3(d8sF2W#UOj=#>%u>h>|Gwmo9=~a${i}yjEudPU4J{CmFRVbVckbM|y3*48Ud%3yhrp(C zj{a1d7~oL=tM^HK{f)D6-3>P(E-qeCvvzL;$A9|6?-b#!?kOfb``CAgi?-C3h4Nf8 zIc|%~ba>*00XTCu6tS_fc=q|15gij#rz@!`7ZnxavrpFH)Aeh;ZJoflobpM*@B`!c z99*z)@SlM3388`(0}To2sf%`u)rvrf&fz+DYu^;-;8aBcsIZX=$$`1TR7eht9Xt9g z0fppS=KKuIQ6?m+dp$>HM>mzaeh&+1jYjmHbse-JA%5G2%0rcAXF2=x$7ilt`;6a% z{p{FUkW7GFb=FZ@nhDx(1%d71U|E8wJteTPy+5w}xmW4up7UQED{vMgOSazN7zQp~ zE6euY*&ja{kb4dllD%ITYwqgvEjSdBx|#hVYpZbVI#Y>i%1r~%HByT~$(JIBRY>;l zO}2PN_BH^My!6gpx*)BKf3v8)R12sU=tvd_$QM>CY?$@$-(Q=Y9NSNjF%z6G1OtV! zYAD~BfY@fU!)Oda`Ym%YcHBfnM@QEIWhE#1_YW(OOpe+I7M{U%6Z4?gdy9VtELH)V zcT~M&c%*H#G#cBsZQB!jVq;=!Vsw&;ZDZnzZ6_0SY}?MnPENmjpYNc5KY#kVwN_QF zD(bz(uSc~r%8}vWT~^kvm_OFE6Ka22nTnd4po}lJA}3I_i-ygEwTeQS6y&!!ykqd4 z6=49VgD8j zPw~<Z z{SPAx{wz2=4h|iKA@5WVJ|?Y7`Tlp0 zaPzF_g-X(wRk{01-Ova&{c4%az_teYXiTetf6J|}zuOje>mEbQhkhC8L=dmHn0$1^ zW6Q>?$bwau6?P$&spM0LvvQiPP>jFZ2sVX#T&#$*E7~80z%O@vyA}TaE739nhLSH1 z#oa4ec&(zU_ND*<2<{JCok zjB?l$mxS(H*Ne<&U3p>W@PEHg<_`xEBaLEF^(+x*ec-RFl42);Zg$|qd`|uUGEXEj zPd^``QVw*eVTG*yJnJ&~a8Ooe}82 z8BMtGGJ z|Lp%uKhI`+j|UomGCM!cb~gFa{!>xa6nM9ZbbIcRi5&saIJQ#`VTI}(CRYSZCKARI zF2o4TaF+SgobJ&s;hL9PVC;$|Sq7$0wk`1gt4hyD8IK#P4(M88=vtwvQ2nfxGJm7m zWrZ}Y!zVLq9&UI0Fxt2^wUsASNw`L{7FsISf!-Z)Hl$kK6w_5u4^>Swu^dxU@$J9k z2^R<*jFCjEob$)UCm|n-jhTT#kJs`YDkh%|es-1}0V~<+>Ad#QbRF~$t~U6cS@y#S z=h=OynBn3h#FUhfNnR#dQ{Q*b4)EYS1d)bPO*pho_}Qnvd$j!4iNBR+ZtF9NL{UHE z!k1^juDw?Pyurpu7ZsX+%w}n>Jg?Hq%X+q6l_OMPHqH0K_s}8vF2^}M|5#QArB%4h z4blL>O9VJUt}?$5jy@5nXpv;%i)+Lr2fZEA8L9HC6VVIjf2;g3?-K`|88E*eKZEHZ z;DLlrx&EYmpHi0J2df49hh|~)`8r-9vGRY~g0XLKySX2I>G`a*1RgZ;E3_@)AuCxU zL3N`S8(mf=LbTYI2W=XF>syd7*oCiQ6KeDe>uoCKU`&nRfCU#_SWUG zYBq~v=m8IRs}CD0BGH$*@NFEC$9pq~L!Z|Fx)s%JVEVa@g2`oMnz@rP9(S#yO}SfJ zh5BW%jkp}cDEUC;%bUfpu^1Cw4*w0Odg7CQ-)nt5;TF^}L10kNh8+Ge{IAHdwm^g; zrkcj%WzofXAw4}JXJTT?l5D3@&a4O#U49v`Kq{nTXpa7R9cUZ{0*ASm;|5f&eZko4 z>k*oIp5HkUB(=lO{ymtvk=sH2B@qmdxbQm}uf(pc=VrjdE{CVP+9p;^VzWJcV_Pvt zB4x*j|J32W(PE3wan&H9ptg2I zEa3+Deg`2Zn-78CsUumg$ zM~V6DJ@t1`Pp(l&wa}2O8v*zqoB7sM@6EX-DyW)vShV#jYoP4=8kg4`I=gP>f5!el zg=H1U9XV+v1|O585EOi3)w_81v%99p63cR;!{`PHv#Nujzuu@;TcUs-_4$(5SSBEV z1ZV33G-E6RDQ)%#qSj9IQ?=a6o+I1k;1iz?Zw=Fc9@`z>p4g3pO;>uLfD`Zih$bW? z#ANFd(fI?pF?C-{TibNsDu3@+>NUOH%L`=h-{hgqv~@09-^W%V0vA0HUG2tF@)*#j2E&6{n~cBFUj1JKjC`O_@7%dnKcUx zX>@cac!RerDtJ?<3==Oqn_YCV^3@#-rCPb*zYbndhs>IcpAvH5YSXL~j$<4R%2Z>U z@wI-{%~5lqXcIhhp$a~3aA1He3SukZ4%zPd6Kb^fwAy|hByDHN_MZGnCtE6f#zg^_W+w3v z;MBfh9tyOTDH(#1JNm<;Yr?5nV72`@GJvGlPbNWFgNYRU2EEjG!OgSk(=nk(%fxf+jdoYbopZaPRq=IENaKd^3OQf>}ovKsRkY+db8-(gGzZ;gC2WX~N>LW|M zd{v(?%;%sa6TJNxwB&Ep1!4Mcew9sagCKNvy}P2*jmgMIW~~8-R4iBQf7;u6F%WkK zQAfz*vd3-6rPvt(nkk7_;o3?G;0%jzLlDUQ_L0dK_L;wvL6+$ns#HqASfA59x76!X!)Hkz=+-skrO0- zvK^%s9#?G2Zh7E+P$aon)z0GK=a-6cglb>2@I^i~_6%ocVdzlC)`b(KbVhf05)-W< zU>>Tw(Ewq=AFbu2CbxdUqB1~dWv>VjQFT7jA|TZlz=Ys8Mck{xr1Qz7u7=$bFpeqW zK*wwnZdRqGrjb=||Nd4;C$RB#FRR07z5ng+jBNMH@1!gcV$d}Z+~!M0?!C|K763E6 zU3@9+?o?-PkEZ|4P*0udH#fno=aTzO7#CB=3IFzP>iG98DP^4!=InMF&K-s}kZuld zHM)uJf7K<5&Fek?AL)ty6Mqc?Fuevvv#pZ;x@)cHl3_!t(#zFiN-*cnS4exh@3X;N z{477KnbAGrKd?vKCg^wILA%@WnS3-0i#bvJ>3xSta;c*FLXjLWivXM*+{VkR4a4%% z^*#6f1`2e%@DI$bOm9%m8@!NYB7gjZ>S#E8$l$(zI&(uy^uU%Uw+g!{>z-d`+r&~^&DuLF-Sgz3C#Fm_lGE`|MMV@|`Vi>BnO>qhW7!a*zgyxM@>U5MxEb)*Gbuv?d{r3)LJjplGW9? zo*Yr(yTN9aXYRVl+%?D~)Ly=Cy3|4cx_Ymw4eQUZK?L~D*7D=>Dkx~9!SLu$)4F(X zT=C@n0iO!mSpJYSP9rPs$(uhGbrp`b}BRZjbUbb&FkykdZvvddd2b9 ze(107q+uLk$I<$IYz3Rpb*v*SE28Fua-FvpO=*Ga{PQ;&F z#+7YQoNWQ#yVdc4SV6s)X?}VB<6I-*!mhDq*bsJ z3RlH)8B}UncRBc#N;mFu_S{54HqQX!&EXS(ZbBZjS{hjwQJycCu(bn2YM3l386e^O zM>`8#0D!Jm%|u}4f6#a?I5Pfo?7)xGtuuTheF4!++{v}n{~Ob50%3Z$5$Kdn?1{s^ z{V!(ZumouX zefQzUnlmD5{6RQ+XU~4->^en)31fM}`K`oN1bX zf%CDN*Unj&sLS4zw^qJMsd?*v^+dlU$nzE_D=t4H~U)!7IZ>6bR+HRww;NcQDU!Q_JT_20Zr!kV*4if3lLj( z+NDm^t0oWp%cz0ua?A2k{(ngTRlVQcdl)DiTTf8|5Z;Mz_m_ynXAlj% z4kgI>H6t>1TDt?GcL<6q8C+5?r)$lGTm(wh{qQDFfDh^||6>O%tfRpn3;XXO;$!EA zo+P0?!-Zbq*PlTDwzD(4bGXn%>*e-F{*`)vMi0mBiV#iD^CjZesZQc~OM?pHZ39`B z77f#(%-TBsC*PFeutH~N0$Yrtvv7U4xPNy;5nqp7_lMj#Fg=nO_D+tQukJ3KHB7a6 z=EumyQPGGAo;MJ;w^K!JbjHBW$5wY5Q1Ku`Ao*>XO(|t{%AvwJy;ob@`AMCezinDY zT`Rp_sH|*)PnfQ1xou*YML(mXjQ);$VcA&L5ml^|yA_wJx95e623Zj-R7?5K7aRFPIAh;hy%CJIvN~U@>9V*}!FP2koRS{SG`k#K50I`Xe3UQt$ zz8g7*b;;KK!_vqT64dvXN4R}Q;qXq_bW0Gfmj0o;-s&HxM952aK4Bv-u4PNDk|#i{ zZL~_NS9XFMOG#~eE8w)+g?>DpUs>&>_)key*n16WqT?z|&%&O-tXc17u>QL4;cgPa z9JB4|Y?F}F(kMJKxr~DAUd?;-NP;7FJre*iKJ6Ep!_b?r1E58`{a|M|>@n*0ajyo` z#H^`+AzyVv@sU+c$gc!*+Zwu~V`X)?XUj!SvT3BcNHxJ4bQ^P^uIO;btO0GA3JKSQ zybMdG^G{wB>*Rs`Dx+|OHpkOT8^SDhhi>vuu?F+zz0o(uwS`|7de*)Yu3iz+@QpoB z&9yf}4fIjknO)-x0u{8Grw?e9g=CV_yY{K@7E+;6*0jP@s2ZRl2HiA!fGkK%$`@D$ zdUk`jL2e9_4ofu);{Ss;l|ZjyCxI*r2pwi_McM1Z)jEE{XS$6GArbRPfbWlv%0j33 zYaZ9HRz07M`~TP0^SM5=G2w9jXWRqAR{lmLYBQ2pE)zldy*&o({u9X;Ez3z@G3?wu z(mys`IH_;mvVzc7s}k|rVYFZEf*%3!l~#rR?ESj$E)pCa67t^swo1hNv@|#clz8&-NV%sbVFv(W?{-9GlyrCZuve+U3eRIr z-yciveEnup+6m6i|GS7$nE|M&xNfQRYZQOi{Hs4mb%5Lj4@xt8-JC6VqtToxuF832 z$`FPJaw3WPLFw1{VMC;T;U%re8n?;D?N%ACCnn+MrHc{~5y5!)(CidyK|g}caDLp| z3ixVLS13e(*VTv_OZt(*@Xr9w7er+mV1jHBP?mr6iJ*@oG!jAbcY?zwlDKb;<=hn!*kOkIQO!HX1lv#M06Q91%WI%Jo|BsZxF@yV(dOgFpvPO8Iqo<(9TQ z_xEf;ClSX?Pv6{SP(tayp_<&NWq#J{!omuryNRw(>HG6zG^{)O$i}@Nh zg<6Jjcd0Z)mNj9*v`Z(f_(OC+Rc{D?gtKl8u-)b96eP-XJ7~!^#2gjXJ{}z z4W1CnI9hx5vBt~Q#^?JDRSRt`>-O?_+^JASN4gMWwZNQQ5EdQP9Nbuo0POcR%Id5b zVr=MzRQ>uET_00&r=HH`Y4qd*l9F%!=-{ERP*_k-}Cs}dbrr}`_w$niZz0U_)h;k=rY@>|48 zI9;xFK)SGO zNf)szm(5d;HZZ{lt7>RzX`R5AR%%saxVgEN`-xE;zpKSn8C!KY(oVHk7Z&b?$Ha6j z|He#sC@3fZ62yjy954&yUTv@fUd9;!DDyQ5LJoUvp)of|xklUP!>hcErgQAiQd8~9f#{q|8894Fpf`K?PR)Cp0R#*_sXX`Or(KIRe)>ibe?hz4Oy0XF$? z9fO%po3T?&*hPs%gXs0}&Q+0wt8;#SX^rFT*aa&7$fVKmr9lUo(oJwn6E_qPYBYSy z7gzHoPTtSqQrzRR@_3_cIrQ&S`*)=8KVcx3-$TrUe;~Bekh6{{UBM;1E6I81s;T_k zMo-U)c|O3Bq@tk|VlR&)E=i)09SW_(6$}_(Oz)9zsIYY|t#H&xtxX0^&9NeS`*&}W zd(TEyH6u-Ak9Zw|#BqrKU9w!(R|f1IO;#CNmD$L{*Tr-sQ>MXzn!!qU;-8jS+%Q2m zoFwPSvCp)_t6;%G`0p0KW}7bG&(f8^tZXpTPbLd)>81{0`4SLNnDO2%u~9VGroJ@m zc25t@mx?o^Ym%TpF<@hmo&k?FkUdchzJTMYt}GO}6O5?9pF=P@KfVn$bVSvhY`3&% ziU+S?lhUPOgS)sdGM`nn1zRkKb>J+xYeGRMZUvK32i@O}O$dK&XC8#gc)^1QH+ep$ zBOgs}xL!?kyMSakB9+QJ%+v z)kHgRjn9V?xQ$8;JQA|=3 z0_q)N97G|Uxqn5$afJt-hM6+yTpQv=K~E&pDA~$Lnt_8P-yIdqH{tMdyI9T!(vpms zFP!sSU&|Gml}ds?(!-GnEs6Z^XPK4eQ#Slwl$9|o(=!iEUs_D@SmTKJ8`##a&EkdA z4m>(p=k$z`O+0(L%m<4KJ33x@J-7WmbCr|rQ8cu*t-OWuZYG%A38k369X%nU3u~&W z-OBl^8gA1n_590vBN4fq;1t%X+d~Pww%_cC#kBRG=o23u9bKu3nV-7PdydlnKUx4o zExjle(HB&ajF&uhQQg2B_7RkU@^8vA@q<46U&3$Mwe00MRPiKkv~eOHzabMsW7Gbf zt!@V(SL5^N3h;Z6(dPOwsHqVe_ZmV2MX|qvakT>u0}tFso+Llx_%3TgGMR#=G{+PvkB3dg?TeowLfA_q|0|R;x zE7+6&kRl^n{n^eGKW%(Z-cgCkbu@Oyf_%buq)T zU}z%*_9$|OF3EwIt_qqnM}#35Ft+tun80xEkclE)#>-Crp;M;SMs_6@)uptNQE$)^ zuVralI#U*UM6Ct-yy{Zsll;}or6h_fKnNpBNcH`O(gLGq^Xk1*PtSCp$I?JE`-)Ob zX_z3qz!E|>s6Z^?y2xTiDCp-&krJt}Rx|f|cKKq$`n$asET=Or+y_`O8kuRrt@<|V zfI%moGbWkXPy9Z>g5{Y^_I6E8jH8xfo{(0{r!*YnzR?|%#B>gvqn9cdBLn2GjK z_GS}IRjm`>1BkvbW<8dJe>;PPO8&h6^Sy|wV@dZH$XwCVrh0_q4x55iLy(oC(ALuE z)U|zT6;wuriT&QSGp!9eAg9fl5hoN7>s#!8dmQvWHje1|coi&|lpaJ!RPw2cXd7-d z{z5Gb_}GuXBL};zGwOCZvndmN`71h+`wJMqEHr-W*bNy@$`00O1yIihbfJZr78-vI zx*X{q^D((~-Kx9bxTVXBIiRHNA8Kyz>?233=A;G}l3b$8oGp8zVJhUW|uosg-9L^YJm`y4!OX zJ#Fg3v@gz)m%mFhrX};9;pTd&0K@VRH9=j)l$s(OmdyxiKzD=g7XPO}-L?+JLg<`T zn<1K|hgFqG^2SViSyP&I-!LndQe}gQ?#(p`4_ChM!5^M~1Wv9G~#cIAYsq zr-XX1o8wtfN#R>}V2del7zskaxufU8%%^n3Ka&D)Ti@jNiP=115|P8*rNDz)x0TFb zsjLELumeMc6pVIlqbKj8D(b5ozmfBrSo#hM7c3}%`2-RjwILAP{jST%Yz@p+f(#rb zBO{Lamia6TaZtc`Sp$7zkr}X@b3z17g*lr%;g`gTYeESI52z5;D)3jLND4RN2|P)c zjmX4BpIt1Hj?LN@tKm?}72qCfxdcGs>5S{7!_81r9D|0-0WO`Jo(@}G?K%>*`95dm z2eMSR`j$9H{9BL)uj#RPrYIIw6tDFKLHb1tEtk7y!!hN9lIo?3{IQwPb;k=3#}bxP zR1`NsL{F-0cz!oEomaN1iEX+)bsoD$rSmExpLED5fi6ez?X>&Bh^*3OQ@O-JRYz>5 zVYr`7V4T|k*sqUU#E@Wd+5vUVb}PpZz3+SXfDQO|Jb`=r z`-@Gh@B!1=dI81TG3@7cw`r5BoYfVtg907D7rDAF$7jOwWP8s2XeEW4vkIp~*0Uw5 zS%RC`oJkB=k*`0QP7OP+mP=K-f{lYMoi(Y3)s224U!jYOn&DQhw(9N^zYSp7p00R* z&$jIUI!MiUr}f5JIG}>%HeG$4q^m*-+6-7(OY0o_jvXKJ4TViBO!rbrz6N@T9y`qR zBC6ww<*ol|{0y9<^qrgWW>GtTq4{qAX`38ycsq1xQGzj7ufSu{=UyW_cBG|u=Lt5B zD@jhsMRkD8qRZ}f#BvyRF(-!dFByaErB83C@2+ybIeaewh`!(Nl794;ak)WVIRY9u z5=-)n)W`=tHI)0inJ?pCfh{Pa-}d1nzxd_ET;0Gd9P|mblzOWjre5|}oZn1ongdTO z!TZN@q14t(ck5dU22-bwJ^mngv>|x7<2PYTeCj{(jth>|1evoy?J8LM$@SrqI<4ID zB@R|`*(!kp$*Yau%{PyhH&_HRBjTF5pE$9G#c-(uu4~q4VD`GN3!&HH&i4X!4uLZKr zZ>veVK^Xj9iwR;p*@-gX7^y#h1u;v$Dwn*xMHI*zwxBT1?}LFmgpM`{vEXS%QAFs> z6@el8GG3hHw(M25b_s%@fEE&26wXGlBi>ZC^{RM>_2Lv0y<4>J`e5lCcL{o6cI-`BYw}nfkkzGFKV*<y zu0%B}ma)2oX#!_UqS}rwV$GK>4#u>)8n6`-Y!oOoMsPDqodu5Fhzc#6^sbP|4_;lp zv62I2-as|ie!05JQA6n^Ev=fZhHQszj9^IFI^|0M%5{61s1t@nd9 z>o*MF*A4a$n77R{DhD&R2w0tob=5g`GJevS$Q;YB>UqRQ4_@xTt?==3NO?4qae~ zIHyC)6_dPCeN)ew>pq!x601SIHsQ5L_+g02pR-3;ZrlVY`=wPIVoA5_Pi)W9frb-aga-tg&jEd_P@_V27jjWA_<__y z9T`C`V+cKUHma@<9_{0^nXnKQ=dH{JCARbJf4Ow$>iSpyM#*6J@S27ZX`xanC_k9@ zZi~KvDnTb%*&rIFEUFj_!IA~iOkJqcgM#{$%RC}Mg=l}wI9G|Paz2<0D^tFm4oG?_K7;k~5@ z9tz?Lhgt8w!9o8OTT4H$619Evs#l64u_#lQ0{ibss-7MEQ-J6F$*hYy=M#yn$V=OJ zpU~F+&5^-D`j95gnZ$!r^k6TusYkaIuBaf)e>cv0p0wi`@ORQ9EQAF~Q$81z zUOJ5$8l2hvMvO0HxdU$mK2fdL>MpTmwpgvrq#N?G4G8B9%ZpbJ&-pcF*tra9+ubO+ zSNGpXdx;-z-;uFj^v4+FziFK}UGTV%WR+G9+D=uc=cDiK2fXrtCuL8M_ku!U z%BsD=op0mL^cxSTM>v49D38=IFUz+rV9t}+zgAH4hZRpJ1RUw1D4G)@6!4k|O8I>k zE6vPD0^(P~%Sawz``Vg)<<;Q+=5x;DD!#iP^NJeI>x54tfvhy{j%b&;mbQCU_)BZ6 z-5p0;U?KhP3xL1^ru?`5I48f`TM`#V#iFmSs&k=aS&IHg(u4`2d^x2^Ns6yAxCyt6 zmTm{Lvcqx2j+DT)VZ7)2pk4m5i_4o2f)6LuBpb>^cD!Q2`Xx*-5e%|Ozqj=D8lrkf z6FoVmzIAljI1R`4RB>;2vel@Vt@nHxPY5c(>nV4_EMFulfEg+q*0ie_hjozB^CnJj zdjom`5yPZ(#)GQgiu_d21Jj>4kHg0v;J~|?ZeW8 z<(SZA9M9Mxj;dD=q;wE4dkgT-k-wBg7Ke>*sG&-(b<<-VWjm8rCC`r}uuPK9Kq5EO zd*NiZsBF1+i3ITO(hrsd%QAu;p{hfL@d9!kEFC}Wkf7%dib%?67ps$Sak&W|b`2`= z2M2-~B^cORvRE92OUIKigSCFIMBe{|_D=w{`m+_4@sd`MqgJM&^%#Vo2BP{UxkvW4y2jnvfv+6)}>Q+2R;BDBGCRhiC~C zAD{VO?^i&isHiYDbiaI+XsO_>iYSrvip25L(eFv#bxvUY+0wc4N$8>#l2~16h`@8S0&i%v4L@41YS(*%A4jtkb<(5ig)orn_~j#f9fc^E&0R%c~&bU8#)i0?1wO z7r5#hx0_mu+K%J}z|{Dgcug65F7wtaa)6`ef4=tS;3kAz=94_X^#$a+aE9tbu8_-a zWgGciHhg1s#>b;CSOKxc%S?<&S&XeAYf>Yv>4Viz_)C0SKC%AX0R9yq)^X=`koma}1QZo5C)+sGcHQm=yj?MkTV>Q0W=4W`HsptP$)zt3 z$^PmbS#;NfZBdLmshqnvgCq2}xoD`JE_;F_(xqNEz{3GhU0~>d>KaQn;QeN#6Mgn) z#j1I-CzYuiHNrG|d4EoBZY~ZI%r!-!{y}qYgh+$KTiU_F!J@08l$4ZR$-)Sj07qlU zaj$jO(X&p;xfkzLO`tjauCju{IK)5FzIN@BUYkT&D(AP$R;Mfx{|>!Y$6Uz}#RUJz zHKOB0P_UfS5T@aJHkmgURxHS!Lb2oa%+6&kfztzT>-}ooY4=y=wIFu_YwzvT$&5G; zewHId3^ZBq^Sg2G_7DHp`$25&FOqJbr_FLT8P}_|W>LZ2MZ%@p5qv9@q1EHO>jQ3; zuVY4KFHfl~R6gi&U^{3wz zno52TWQ%ELvfq`!%*9hX5%z7qRxP7`V@V;$51;p~l%@_zruoYni;QlI((y$mGB=fA z^TcUmBRskKj4vc5+#n+SPtO!7N`r#@=;NteLf++)Q_nte0M)|<|3qlaPQjR}3gnqJ zZBfb#zQWyyLa&c+<=Bk)b;ZXxcyqA8Ed{pIF_$s(`3g1K?@fk*&;HP3xi=g5z%GCx zA#&`a2?X#v>H8C^eVm3}-UtnYhaa}UPmUzQ|(J7@P1b7e|2~oli@j0_H)g!IzP4QZ>u(ADDbw(d* z0uN+Jys0KHJ-&>Y;nJSK<%&AnK=*IB7}-up`f^y90$!`N+Gz7byY^R-7Q3VYM=Rs2G9p2tLI?%drs&12qB$BI1p_avuxy# z?{?%D{(Cy~3?gHtas^9YX_b*#Ln`8fazs13#NGA|Y zzj4-{@-%ZaVN-oY@dMwu>h z7is; zO)GVi^=k;KS@3D;Bo5J}+gE(?FYH~aHhqzUPrM^5;8U(!5qP`rMC;MtzoINdl< zi6w03mJGX?=GxI(Tzn8IMn*=)ef9PX+Pw7YrS}$=^q%fbDH9ZhtrUr)$n^z>AY$Ye zeO6bJYOJ3l03E9nc>M%05>@d%MziGHd>_b2wKoFrqC*Q1B^A}Qoe>ZLf)Cz!9aTiw z;Yk7a;B#|xF3a6dg5*=35J|nSw}?V~Mv_y3z9VsiJNqgtB_3%^Mj_#(M8|N!xdI1B zF>NI@Ytr(nKr7O&FP0;}CuUGi?KzEyj$W+ua^OVufMFw|w%~xF!Z;7bp8MdfyiJ?W z?h;a4g@o!Z9Oj)BREOZi(T1_^rjLw56Rikt+KW)@h+ATddyR#ZpA4~Ejyve;RoAfu zddBGykLT=#ZlBjp2ss^gBsEK6FSe{_Untekdnl3dY&e3Cm$1~fPOweTT&rD~CNRCZ zp4Exp`4Z45T+QD!bN>qYZPSC*9Rz2;dKOAv$qmJe39h59MDGq03&zWg3g_YySxic4HmejP9Lx|p2rYyo7_a*<4Z+DE6Xzbamb(A0;;}z!` zj|x_!vfvR4ndBSFxdMo2IO(5s64eX3%se0y%NDD%wUr*>raz_{P{0R6T<_Z08|9Z; z{EW@Ou48A~8LNp=ggJrPbl zI(mNh;%If0s8*So*-KvdWw%_jne}?%u;42QpG1!ahgV)+zKoW*(28?W7nNBXD@n2a z=GW74TSpQ;w%v z0KkD9(Xv#*$mf9mVA5l=tDNh9`|sX#jEqBpX~Ztx>HU06{*})fCD;R~Qlk5f$O?6B zKZVCA$#v`pYk=8lg32DH2W4X5`Rg(1NUdMz@glD$CSVE!4EY&JX#q;!HgZm_BZK_8 zs3k3zsQtjQywIE+meC0;@)HLooudV;@8geT)a79opWf;&&Zn%<)D}1?w9J(1^xuka zrADheXt>t3j~;#YmmZ^AdXB|8$j-*i!>7@9A?f+Ykdg!C(GJKB5IkI&FqX_w`M43f zFNC<-x%{?jr3 z7O{W$RU&;|IFv~MHd9u;YoVR#f@|Qi9ul!qzbgdjH&S&!kj0!JB#;6o8~!Djx++kZ zQRtvTSZ50WAH*8+Gr!EN-P0ZLl$H=(@agnL!TPO9oGJ^Rh>L=7|46g@fJex@G8%4= zFsd{!HY;9+$3BKJCYKn@yMtlEfw%Ua)w^@jD6BsWRv%gz9+8AUcx;IpfGsbKxebh* z>MuR+=Ovx#p7-PtDM1&5i05|H5J6~3i2I6`hm1+c&1{dH1C+8nLb61` znBAXX8gan%Fe2=xM_9&PcEqtKFBD}cDFxBTYFjp~>jo?M2~4Kj^z`%&DQfXcj|CaJ zwgS-uo|$0JEBfIPNs%Xi*Vbkmc+|-E5kSz)8wi@IC43VBPpa*DB|swB;FC?8%oJ71 zy!Bi{#XCfx=&$i|Mn zv6^4A^cLB+XmG3ORkbWb!)Izx)o?v529ei1wbCWnDH8qane6)PWA>2a8&@gZoTy11 z?WHyWsYyvZb#ARB3*~OSO%q8CHtSjIm*SOL4mtT37cI{1P>>FGa-V;GwBU-=BBGBU zc{48IRI^qh*v5-a?yHnDBAvSWMi=cFhnYG;%03l$xNk;;BXhbUAQ}QaUPG&%m476K zcVZQ#z}7ns)a)635^TbDT4{0f(lWv@V6(|22P8BK8#8Ckj?34LzU^dP4kQdK>-%Gi z0i1Rg@`q8UPa6>@X_WcFg#XRCd~74x;e^ghSJS&E^?NZLSF2Dl-32yRX0!$5g=~5PxZp z{$5W_ecdXJcdV>}P3nf~GF$g)l^3(f_cs=em_pol4&>o*hOLNsLbQS=uq<vs6tA>}pYZ;EH|?t+FCT`H7!ojEDt76d_0vkK{vRY5 z&EetUBK**$J1EvGN1~8P*jjbJgb64 z{w|>J4)7IyM+*$UTL1cIXltePY1{Sh-T1ri_n@LU5u8Dm9?%n8-21**`41EcYH~U^ zgqO02t^F4qs6Rsc^+VD4gLuo}PFS7-n;?soB;~D_7c!_LH_dRYY?EH*k`fkk7T4yK z873#eH|uTGERIv(vSpxD={z?-j7hMQTd&zR`r?8|wnh^eC%nX!T9ig?kr}FL)M)B< zIA_e0uEo~gRnFWX%TLv4Mgq8n`9AK^E{q);bimD+&dS2bc)4m&<97C9`vw=*6CAYF z%=Bl0F10Q<a}l{;Z;}?$zNH?d5mU45<`rUo!%3kmv!U!FxtW zU7RCKiMIg(_hr=S1Kb%aTR*5C9U&im9t;N2*~plbDx5KDIFQW#52_-m4x2R2BvRG7 zCTy$FTb_q}a*8H_*5FYEDwD!-v$C5*6rn3I938OIBl8HTs=R3mJk=kma$?ANnSq@` z^zlzXYR(9(lq1X@z;GotGy0V}q~&Y*9vgVod|1DQfqfQ?LL4ycrms7$Wwp7Pt=o;{ ztP)ru?xkS!))M=NZg7HjzYN;wsfy8kG7PIWYNKCr8Dj-r-D*W9wyWq;UPugN+Q110?Oo1hl!Er$yeXYAY&i`2x$E zZQvk$2q#$MTq3&+6p$^!oiLL80d!bCFH?fo8(HakzRv364KXjXGNeimKHU^-9!Um* zl2|ns@r2kH=9akVK*8pB+44WpPTdbnq=oyN{2}d?+Vo=W?d{T9bO#kPYZioI>D{(X zR4bg{nr#;em^oW5wD#SbT?TzpQd0Qbu1u2c{=95=FRTBIn3@G+nCm#7p;(3MswIyGE$`Bo%FO&$5jVsjK%)^8X z-sF=e^|D+v)J-t!N3FfJ(C`Lsn`YXHHP%h^tN#5AI9w}KI_$6G>Av(UcMo2*CPtCQ zN@N5~%KJSm(z?ffJb!rVG`hYdH8jkHNeFu`4lcA_qxGhqoh!*dm^A1!RJTk=uPrX2 z2H)inORX$cX@Vyj{b}_|W76ZIBp`4{^V-?T+Viii!Izsr?ry93UfS!qQ+Pph)JAO3 zBdBBbb|fd0i`Q`Bve@DuOb0#mN{%9%mR{2;#zlS{8-H$8it=;Y-@~wV!X(5I{+lsj z{mud3P_Q|S2<%Ddfp1T8M=5Lw^k+SVe)dtrnqK9@F_xM@5^>wbjI??a3Y^Se#X=xi z`(sX3LC})q5*}O4|3%bWg~g#X(W1D!ySux)4-P?sySuv+2p$G^0>RxiIKkcBgS-2g z{AZtg-)6qyp}VQBURAYLRSGbsNfEei1|h%5etSNu9JFKtg#G*6hyNi8Mnyh!j8q9W zHwRW_UrvgxXA)}Ib{!^bYm0ga1KdGHg|3K_%0@1R3oDkKf&Jz4n}(P|i-(X0f7O|t z4!iO-^vE_?a8c$81mNB58*{H8$TD!O<6?MlJ->cy(-U`^NYBvC=4TmHU1S5^D2_Xb zYsklT_wvv>tk!q&jbfGLcvwZi{<-XRSE+*2qE0u2Jx&l?&i^Sv))7Hv7KYUbn^`Z; z_fw`3$O(oJc9mT=F1y%`xy%%~Xa=NqdQ!3slnpiy=(er;Enj9|<)Llj13qjS0cH+f z=Y{Q}b1jQ(RXNcQbqamts?H7$};yrA@y2=>vBpV3g5NW;!U9848GcpDmS?!K7W zFvtZytXREkYHCuz(fADFOJU9HDx^2DZ3&j!CXS9DVye-W4Jt|nu_NtmmU29jK#x#N zYwEVo+i8?MOQs%Z$>SG~QSZbT7k>{Q1Sl~B%tgP(zL+--9T>p;bc#VicHPFPyIQYj zz1nPlhFjJ8ftTjbbvhPwd9*7imreI=V#=*UxFoXw=VIjjnVv@ub4W(aS z4ZRzfFtq1^_cwF;teGt~6D#rST7-r2gItjdlaZckm+KeMgQ8iwVUnRUxkUgrGBHZE z6dw}|c7N73&9#=+E|Fqrp_eG^yG8PSDRC{WGT=ZQxiM-yXKhhAT$}WJg+6DA$=nLx zxM2g8qwrj2ON`ea)Jyk^kkgQXanA+FOBDWkFF=uSFAQo^dd~M8ydO-g$*fUq%0s-O zI!zS;L~w@z?hcP`Vd(mE!fQ!uEED&Skl(ngyLmvR4uz4?hTSTB@E}rNp&>O2k#u`2 z@%ks4(>uYAJ~moRO56cJ(`X~a!wt9xp^hf$cLI+kPFZhx`g1CXT=40iI8uPgYAJti z_E2@KbRYHJS5y(b1{TGP!_CA>iVInBJQQbFrB4lrS}9^pwq6RDkR3=hnh-rsGS=@r zVYh%X#&_&Ls2)D!YwRP9Jv48QF6Fp*Mt!!LVT|K!ydrTtlZo{Y)%DlR~&R~ z>nbOFVc)wu;tj;6U{5$OlhO`0x1#TMO-@@MaWCXcO3)+fvjBQ(kAxsS!3WZ)qcb27 z*!M?XapY@W;LCBWF&kg>_g|h(<3806YoFR`YIPK7aNj*AavMw_VP6=8a#P8%AMjFj zaJ$Q!X!^HV$Ow>JaasU0+XVTkli&I@j?tt#tShPil9EANl~`!U>f9h^6B@GYGiy** zV=r;JIZ+5S7tv<09J?b;o=oG`qhd`hYNu>x6g6Bq{Pah?m4`9-%$w?3-$9wE9ybvc zm6iHk?d~U35-ItU`Yvd%A7DmFnCtQ zbo(|)NOaw2lnB_IN~2?c3x^F1K!q|KAuWf$HdJj6uuZEdAgbJ zjjTk~ge|v!??}>ZNxr;4T&|_{%w)IQ>k_&cSk9b+4AEZFFU|NC7j2cob83jma^6|| z?7J?ii}8bqy^>uKZzotDJ(>HcG47O?*^bUjE4)(zphc^<=R}6CwV%9NyXW+MT#u*8 zu3;Wt(9_kBqfa?Uz3l z*bCurqYB?~AYUkYpUC1NeGrc}-1?vmhp82MFMrTAE~2W9i&@w7A$RqOf8n4vu3~6KTj- znf%3dSPKNP;3axG9cK!pBOKO9+1*si>DliRZ8DeQ@YoA>goam((1JHWd|;ejW7WP! z==o8iO}NN@pO&q_*NEkt%BF)7LyW&VcFX~y9KLr)4qYvfA;I!WO2U!frM}h(gXw&B z*stZ1=slGEtTe>0;Va~(fgov?WPR_q5fWDw z-^V$Xpp^p`5+YjAuw);Sgu^>kI`@dB(-@+lU7Kpkc!KaVttvGw!-zP-y^d)vgXKmO zV#A<&Xm`F}Uz~f?!a7S(_+fK8Jh%S1vCN_MvK~5hQzZF2R~nk8!lQCEz+U)SOhigC z2`*kMGVXqXNOR9z7~i1D6bu%+`nZm?x|dg~7wCH2YImi9oxa2PV$%cZrD&A#w+_jK ze%F=|%)kF>C&xBpG;A!cDk2sz%QB^EDv$-|2$QxFey!&}#v@RmbshJEkgY!H+_2K+ zXCc=FoyCzC!pzGTqAyrw(D~d$&z13H0&%?su{q-VU}ZGq78Ck_?PKUm(Z(ueF~=Uo zd$NV9GZ86YvXB>!LS%qWP38kMiEO@(RBL#4Cgj@7Y!bnX#YQYZeV4vD$kt;qCnsI0 zjGD3XR6y?v4Q%ty}Pj)wXfEs+(UW^ zC$m&^8D8lEQbK{P?Ib)w>gv03)Mu0pe0q0i)~B}W-_70R2xANv&`{~rAqU-3#W&@o=oa4$wDWn{k(Y%;ek#rRU8`Zeli$bMqWNT zoP}>>Mr6%XN4H~`4GF&2v#-Xk_cr9Ebw(;U;N|7n(Yii{HJ`D@wI_FU(=oDW}ZMcJnGQgZ*w-_M6G)5QL6EXdSrFArrTz-V_328cCfk&eP?5@ty4FCooU zajj+cM--?#eB*I?u{PZ!-|R0Y#2<7^;$M+1uCC`L-Kkl3DJ#x|cNcamAG*N;b-vq` z`yv0nDNnzEWkuPu!Zmys-q5&<1X(OQEK#deYtMtX16SHOOvl&*3Zzoi$}>P#t)!$x zO`(<_PuR!M)-g_m5ZT|nb&1NOkqSs(tE^-KBq^bMlaQgp*f}{lfq++X?7NvKrm&qa zDk`#xFu?;kDOLHDMXNxIbg%EZcBnT&PgEx~|wL+8a;so0_?O@VIV$CZ()pi^9=IHnO{LtnMg*wWLa z9-V^F;E=J(!R#Q=O}Pq3@7bomiV*3se7;DcA{Qw^LMzqBC7B~vF=O1mT!qBhpCpBU zLFJ8vnTe0B1(y|ePzoBAfkBbigV~7h4`Q1iI+naNbK;uqe&sV+AxU`j`l(vGKu#fu*_U9^Aa+Y5+EIxYnHFKF>a+Nt!1(uvBshrV&|w zy<&A)Xx#6Mh3n9Jf<2brWT>I0#v*7Bui#C#6B5t^`S`p3#Y^hWHcXP-pYsEl9rD_v ziYY1g|Nr{el_HS+LKROldg3BvZ1u8!^+tqk@Gl+&pvt)9XYg&1Ack!8dBfjzF*0SP zofk4|{F$6}ATG9qS!%fPrL%I!PMGhlt$$~@ z$sBg=@R2ULPL7^~w5vh{=cYr(9^a;Gt@c!|unp(|ba`KCuu{ooH-quLKN$@9vt*go zyx(ck9|8ZmN>wZ>z@zpn72^vBiH#ZPk7qA=mei_dn`5=P-D9slIa*e09c&CQk;JNr zK{BrPqklq$)_Mmn!oQhQ_E57)8CX`L(W0{G( zD+*1S{=D@`)7P2#jNs8%Z}$z(RQx)=eI(^paV{Xy;@jG<;-bhgd4=_28yEwYDeZ-Y z6m^n4Be1{cr9TG_;ZdoCu1h1u&g_YfO>lo{UocMcTAOTwy#KozI3`eN_uH+| z%|mo{YNN`zECp zU(?ejbf-YX$b0vaW>!v~h1)M7Lr!^ZF5je^mUQ2nVt#AsMr^!vjJ^%m;-gs&L0`JY zA=__l8XU`WNVk>dh+;iEngssp`h0}XZ@Uyh$P6+scrsA0@50tAPcRLD!F-1ak2hAk z+~beF%B;H6PCd{Owp*-}$^iZ~3jJ%X7TjDr*C#+L=HlkfH6zmp={F1+a;WXsBpZNo zH52E4?Nck@JxLn0McKvmK|-)rvtiFHmwRjXn00iSf7^2}b+`V(b*ft@T1J6^n@-(V z*;4&C`xK^CzpI{-sh{dg8_r$8*-*L5o|vTknqOyXch-=5l|iecNf}uDw~*Wmxr|Bj zwMpxrd#j4v%OY^HN^3ABNck)LNW17L^ zz)MLPUtW@l1;*>kaKi$A(UmS6o?VwpZ@eooo;EnkwRa${ zD>v}uD=khFX-JRyWaktI%(Fr<-o%Y9NJJgO7^wBV9he(|jE5 zOAFjuOc!MN(%2+y^qVdqj9Pw$fooqSTPjd(DxquVsryJpj!8brOIkOXQo}iw9Dvn5 zO_53@RhV^SFb((tHsW_oFH&ix;$`9KiRiGrqT8SnZD2+N~{kO`^{l$SmOl=&N&MK=4XQ?ejON3tTk7e4x zRV;_A$j*yyaBEk-SkCxK-FgZH8Qj>54f292S?uX7JB`vpiztZ!IL`d0z8pvzRv+5c z#W%us^EHyC04cZ#O7H#jl2ka*@Mzu#MU=L)n5ZKTiU8TqkugizDfC+YnqNT-YWL6M zTnI>mP)cXkCmczEe`?r%>y6eIe9aePt+8Si6D{N3RG3*bLJ4sF_Jz@J%tqKhiRJhp$MBbVQlW;?*G2xStI}NR*Xo4Cm={K{ngq(3?aI0mLJ># za`n};(*!{REqCq@fjrqIa`4z7;viaIz&{WWHVy)zMm}(Q5uIdFvsRML%4;w4u%9)z zvyd~wN-!hp#Np?wuEpxbZc9ek*+djo#KAh&CJhIt zUV+(AJV-zqyv2IyJ7L9fVa!HnW5k98X;_0hGNPWzWg>Xh69@@}WUUd=!VG|rki=E_ z%ASMW+PF6qrCD>`b0QYsQaZpX7jgl1w?=6HzNCn2)zSFO^nJF+S=>zHvrM7ugTCe! zEXD@myH&6xD~SlTk?n=R!+YrkF2<1#=3keXpl2H$tn7ET@*n}1D?S=qiZ7-DeMAZ#bN+^b}-t(DWuT}6x(w2%=@~sm-m@!{fdGP@fSXPcJEd+Z)10@x#zFL z5VG$dk!t+MQn^_f$6qM|0zhVbB1ImAw?G-0p-g7V3YRhN&(7W)d=#MevKY=>jQ{M^ z9mSaA!fUzv3FNp$pY{&zy%kG8ov$~&Vf>>;=KdvoCV-apS!gw$?PMzr=Pz9|HNH>^ z_ogO{69pZ3btK2FPIPT6wdQ_PULy;hg-+3ND)L(q2UD zP)yS$k6z~AdeUeaV&RnFzHxZcm2y}^y6jJKruE_{)gMW?%(3w*FD~@AuF(2Z15P2e zBJwSQ!`fysdKr0l(NR!n#^-yg^I5~TD5n_)e*s)@^(5uRnKw(TWFtk|BzkUno@)hl)BZ0I`thGvYawa9JZK7# zTTx$A4}T`8=YYuJcJv-BYs6#|zn?yKKQdbkaTgI!6+B^5Y&VTT&cJJm0%NZ7lgh|*O&Tz?m&Keh% zAnz3S3`^XGP%pm@eI;RIY$U`?^q-7h5uO#M5gnBxJ}>{ZMd$P^M&~PqPBN#M1^^Va zaHfiQI=Vb&MIAvvwsICxJ@xaD4;T9B=X2+>o8+P)0?i_6dXjchfPfO~?P_oprg!$u zX*;p{T(ANLUi>er5t+3oRLUrX56{N!ev>i(h>C#=`8{s4*8$f>wK@3cF+FuSILQS zj!O}HENU^5HFN#`6e)tg>R7QQ=hs7X~^WI*%=?R!yyrZc7{$Rl~TXH6+qDFrq zww>Lqj<6qG3nT8z>^822eHMC&uJZ|fwoAZaP~;I(u@S}AkO0*Nv<-HpB-03PnuslT zhEhP}G4tugu`eqRU9u=}&}Hy#8Id_nHg82Lr15V9pGr+l-91P%>fQSM@Peg6v^(?c zXaVnOb`WH02wssy6~^M-^5=AQQ*n=XKuMaV0#ycaNPLe>BE`GjZpK{Zlz4e$=U{ya zU%BK`1%@5RyhCiIga|pRdT^m4yfDNk!w0=VuhQ3*h9`R-5_~mZ?!9UP%Q0P9%yBKoRr!Bh z09oO--AL~kbm$Q&zB-;*e!9J6RfiEyEvb~$s+u_H5e%Mc%Adz%x+UYo0SZf%>lAyK ze_RGrt49t-pD5AqTPlFcA~$Vq9$S!W!TUd6Spz;hQvfhNPE^KZ1mGiXj^~U0wF^=d zl~j}*CfZ`IE>Vxp5TE$@_Fx#gENUD%KOqcpX*rD`b1VXs6Clg@rrk zHyLarsr4jeaVX8vv3P7g2MEs{!Siq z)p?4FoYx8$Kduj#(Hy`>Mz#5q;OsKI5$c-1q9+vr^N;8)->a)xg1md&PAiCM+O!&( zew)wTBjH?|KNpaVIHZ1}|Co$D4n!5jnvhlr-XADr{r}7_|EA|RYt>0L68fzz*PX6^ zJ&I!CkkP8kpOEeRC$fy*M1-SRO}N9UaDz@Ldu>SF2Y5JDn&GQHwAySpoY*@@vNTba z3Z}EVWKjb>{b-+Vdnp@DG<0Tq*Hs@x_GH%{*TZq7JKLN%oIp~=qFs*eIx?N*T?N+P zppu|xW{a-6Ty>xS#p ze!LA+CJ2x{*N?IE$MJsj@-zGTcb-yYDlm8NW!zDU!q%QQkUf9c*u|R}6Z8-hT#JSs zMPH}6`yYcD7H;IOu6H^fGHR)}SY?BYs6g`LAO9pD9L!~x;?hWR;kQvpfB6vYlWb)16Lp)u_yLQ)7*KWMcA z-p}qKJ0n`^MHzEwOzvl!IbjxAXgoNAMSk6%zw0)>M?f@GLdk8O4@njL=lwu#@O=VQ z$60h=9Ki71aFjK}wb0W#X@?E+lFP{-il1D;72SfNvP0}6I=#s=!TtcNxbiz1jSEH* zuR=0!R55dOjTVy z6x8r%yR77cstET~th9D|aB%Qzt1~)}!|Hcqw(`09b0lFB)^-m;jgxQzhp8-H)UmXh zo@{=%D^N*udb^5UM15D)aHfzGl>?q4PcHC~5MS+UB~~4gb#)gFK}$U~n6xT?y}1oN zxE>|tS9GQGN9N3tsJ#^OAH8YCmJSE-07QFKr2{QK>t6T9e$}H=EiF{Vnjp9%VV$V@z@s06B0Bo(bg-kGh2fu|4qdC7$Alk?JeF)LG{y z_F-?Z*az`70@v|*?1P!?eGtc}uTJVIa#r6SJp+oQD4oHfUe<5UfYD6nm@WxPhPJ0` zLTN$NsjgEdTAd%MJ@fm^itbj|M8qI*&YZ?{05W=njdWqBeixJ0U%d_>McBJTIdrSc z`*EJ!EVJ^!QqMhgOwdJ)N%sAhXBB!Ou#@ctb`Eacwb(Yy3*4 zL%zQdsy%y5hn1#pl6_oYp*rTPlHwQ@Bt zkEJGK@r|ZW&!nE+wkY`d!vmCy=y0^gWZh?Rij0&eSXmA-u0pACp1V2rB%be{~CrW(zE7t zXgrb0O-E0!HlUNW(c!-vMY|m@c%d4upY-ADmNZ~Vejh=#b@!1OuH`4J-P^+)Jk!#C zo4Y6#7*Sl4{S?_xtoJ-rxNDnUpjT_N3fH3D| z=rx*D5mnTf3^QTQGJ*NjST4WieZY|W?643!4L9D_N*g;PL7ftorjJ@f zIb(9=R-tpagl{K>b{HsnR%Q|^K2e1ALGhCr zFkwk2mg$R1CAP(Op>jHVe{A+F-=WU)LULue@cx@zo;?`3X3 ze576@->xzr#!|l%-V{EJh#Rc%r*4~rP6$F_%ZUQ(ieKLf+qfVfDpFF&B2nJYB`o7H z2a+q%nZxTQ>TGSf`XoX1RXB*yU@|f|E_tYWQFh_K352Aa;3Xr)e#m4~V4FQBB)9bq zapiVc3zYL%0G_azP+0`k`V;Oa#I7(M-+aZ9cHs1O9!nnivg3h{OvB{Yt-tQ5LzTB~%&YOWiNnfEyh5(?w(zVQ!^F^Y{tp zW7<4_3{J~&DYY4sM*9`@_i08&Gi6Tbz9SoDdHKFy08Ez!O>O$n_ff{0?HJaNm>fZ` zg8Y23-+#L9J~(`;v}+7oum5CCH&~%c7id@ONBLd;R`CHwforxT0W(wl8jtTcLBB;u zM^ASL1+DuW660}M|M+1Z`6Ν^XNimFL=}sGHm5%FQ6yaX!ngIC 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 z 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�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 Xm^Xc|yF=so5LHG(LA*@VAmG0MQEpQ$ diff --git a/docs/src/archive/images/matched_tuples2.png b/docs/src/archive/images/matched_tuples2.png deleted file mode 100644 index 673fa58659242248835fc716290360d4e27ca32b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8093 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 a2g-KTJSm9%!uZquuDrC0RF#BT=>GxW|8BJa diff --git a/docs/src/archive/images/mp-diagram.png b/docs/src/archive/images/mp-diagram.png deleted file mode 100644 index d834726fbf76db83ec287967fd5b8849f5bf703c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156543 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 jgkP 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%0DU!K2Um-JNOvP zY!)0h>scZv==!2lZ(sb$H8_I|(t6TqReScfXNJRPWZXf@u{`X~<<<$12Pmrp?le^B zK^bI`UL|`zQOKgtb$3hz;J4(FAQ{&*LkkN-|0d!}Ut?+tp+&rIfY64{dvFPf6n&su z5|Lep&x2z9Aw@md87x^b0AN9N{|$P;l#P&R8L|pu6iw8XUT`z$f!b@z7;?y|D6OK+ zk5%j)H4r^<0IS8R0&@>)T=(4rpC3Ofm3=XIeNSNcNv{F6XRl;bbN=Xt8ap7-Vgqle zg2>j*0=`>nJ9u9cjybMA)Fb1+@mY{vu8sTn*pB!gD7;ewtWPr`0Z-*^tnTzNdWyMixi~P zD`Ro^7Z+q`Gyrxc3K@2gGO9{6Abz1i<7EZ_AK4jGrVt3yYXG}1Pm(CZJ5uHijap`C zQtQ}OTH0%zkb@LnscrY7G~)r#FUQikgLa|2hDQU~;d;LY?VuQKNP#J*k?$FKl=TZ@ zu#xwngHeL+y3n`cCW0REmZijP=pQNxo_s>DH%11+jL7cszsYkmgf#V%D1k~Gfnf0b z;aki}ta)4rR+iKU4U6SthD-f5iln>2FfE@!I^VZpx?kPVnLZh3OCfsT9*43xQ>-;!`4^27? zMA3N5w|Ln^>jFd`5!UIhA|-M-IVPqbjt*5#8&2?*4zL86gnw5oG8q9h2K#=eZbR_) z=E#It&s)$^rb7o~KM)n>bGa4nqjbkKZ$|U7TYyFL919HStRudGR4=xG&T~Zrp^K^E zr1V4^47z&^y%{p85Z*|iJM5(-gVU|{)GX1HFTT0jB#~GgH`23aakmTUO8*5t8zr8_JnsZ*oZ8b2y#T1f{E*5&oLm0OH5O}(q z8F@XQO|$O6UkigT;mJs%xY(S~I%UtuNQVBABaWm9dYtBkrJ$?S(po5zWI*|?I18NW z>U~<~?-K9=+|pKKuf`UfpdwLnq3JI!pN>XuhkVTM!k69?9X1Y)Z@S=gIlu(ahnD|h0dHwy(T)VI! zYi%w!4^GXg-ffjWw=doY1N@rddtKN%c3P5sWCD(bIxAHsDKZ)pvR}dAuYvRDWp6n4 z1?hs^7qAm5P@@dLr`;#=XLyp72=Agq*-B5m_9%}U`be%fIR&|M8Nj* zzcX<$KA?DG(P4%_$JR$HR!5W!wt-?u2&GfQXdfklwaEv4=bMr}*+b?#`>(yw<92dZ zpDPy|_T)7Y0V1o~p~_=Nub}wH)0CMk#ng^*fpGxkw}V9V2*|IX>TP5w_CuT}boY%Y z^?dHWII|G_kR#gwGVMgEZR98re_olBE?e$9(#CAKok1}4h!)-fZ5x5|H0p~z)<=Va zuF*$qZ@(Q?%y%z=7chg7JO~N9P>G!5!LYY~RxZw2uyk=D679c~DRLZuQ2%yJjT4Rw z^O8X$uY376^r8lVB+mqs`r3@}ighscAOuwTweM`{RU2IIfuTUBlJ3`Sxe_YCInbk? z%8UrU+#1LvI`D@9`t&Q8;-^}ax*Rm7H}Es`*NNW9sVKs*!TC69A>Noo{?##Cb@y%3 zMIt372i|7>MuyYn+mNE*=-P#&2PzmBc1V)Uvw9ZQU}I+HxQf0UeA66h!K-xZVjtoL zf|n^;6ScgY-*@mK`tXLNbZP{qDuG@Qsx;cmC**Rj{cf4ZGNi2kfW)Jrm<^gmm%yuW zAQ_o7*FZ-aL~SI?j?MUh6>M~t>%Ut2FP=o4pbaI~j@HmCPf5*cY#9NnRB5t}b1r_ogn zbS;g?B#;#8U(lN)#h6{M(6&@qNN-6<1K=W(fldc!+|zhjx2WQnP8h5wkQwq13uMu! zC{nse^+4=`UKW7*#|p0y=06c_I|cwR+2@H|K#absc@;%EC%wRO``Tq}{9i|nCo&w; zbg4m7Ci}p>im^eQ%QZB;3~hF?U};qN+k)H?im<3&uf9{EcZOujXuKyxZyoSe0s1yN zxEPW)UX1NKq9ei@1rFx4R|>I6S`gvw-ZB_oMv82N_Y>z~1$!0hX3V_uA)^A7Uv{z+ zLE=ANp=OhUR`^^Xir$sd@6t^WlH8HMFfYC}i`$D;p&%6L$%f4sD06Kqz3oTp`sG@I zgSi(i#J0c8AzonhvEFflkFK!-xsHcB3eoOR9EyQR1IW8$4SNI3AZ7b)+uWPXMOh8;b?gh9F5iHuonn_iDwkbZ+VM=3I_Z zIt5tC47LM=7elm!=T&*{6IFBggKW#AkN`=risz0-_xui&WcZcLFXs*#)7PV+YL_b@ z7?EYr86+vl4a>A9FVy`8BU8Aym5y>-=VD->YbD5xluzRBUyMM92=eJ28{LkV{rcso zTj^16Qh!`d0LVB25{lyr3B`;Rf{7Cu#YPV@pJFQVU;#4l1+h;>)+3Y!3grY-O+1tm%~rqcLO{+w{GYaNdcfVz_Av`*BQg+ z|3LbW5|zh&6fJptyOF=0tD;ZBKM52wl3gu9NOKT;8_(pUhnL&X+mo*ah|4OqM&B*& z*Bv7Rhtz5Eceaa1x#r~xR5RLrNwQEcemOiGX}7Wh@J=OZOz9f&G~`>N!c9*Vt(W?e z-PM@AYvl$rT`z{#6UhyxyLeWD{_1noVJr8|S5_z-!lV8eJ@{TH60LO_%@Mnk=e5w)=V2 zBXFk6<@Oq!IrNc&rT_*5#Wzn+zF|Fa%>)c5d|fJR*~G`Sa%znY0HC}&qj8THMGIq= z>%n(yZM*!Cs|ABehXLN`AN31QCsnDzOxeqiu+KX(m zE?GI@k)8;)az<-uQ;qvj0?8Ui5RRR|ViP`k#Rj4ygFbl-*dJY_oKMiG5UihQWc4fM zpID4qaX|oE-!~8HN~J%ZTz$c7Fp>1WB&Jsr>FtU7r~4CT>L?Z5hh!Ah=YXBn{r=^> z?|;iDdV;%!gdN)k8kflGEgya$&+)V(cjYq=zm#_o6-E{n3teZ^KQh~1ufcYu&(s1L z-o=OoK{vxmkkAr&6T+k3-eb#Ro|C@zPW5t$T!XKG8C;=LztXb;csp2$lwYWL5H<(R zUJw5DM)b$@*=)?g%~B(6j8f$tCy6~J-&lDK8-+vD0YbmNP41L~{M zAVehO3r}4x)NQzoCl=c6zd569lB67;Z~oA`2Il^T%+}S^gn_xIuktb_y&SbzPapF4 z!dg@?J55hT1r4{HxUgmdA3xQ@oTMvhlm9E^rA0};)dauh)WABFZoafi8-|8|OAq@S zrp}7vKZA=*g3cw&28zpFk4&0?=QS0mQ`%$`B(T}+(fuk18!{ydPWbOAV=h6f_e|l} z<$3CfBtTH13n|qQ{uwO&k+ue=Lip8=SmH%NU5W?D#3=(|S`klEG8YwBMMS{< z)2_(B8n59OLkrfEJ|3H(4!U z?hd*TD_p-?TH*)^KgIh|m7IX*7r~}K|AFF3b65v4RG;d$qwg18WbY-x`{Kyb?AngY z@ec!>ypIz4QdqC))tA=NfLOSLNePngk!RNz{~c#2$E|)6ZF}{w;3R_+s)9<4fIi>C z)Ii>{A4~UA;U&KptALyb=7WY2?0@&M~9CRqR&V8?&}h7$Vo|;*vB)!)m`i6p?vy+*tn= zwSf6m0h4PtQ_R;a!!xzs#t769;LS`2X`>B9hu^peZP(y2IE934L_VAbvv+uA4-sdc z2p(n~ZlR;@T6wNsd%)@q!qcOw8x0ayYwSxFp8Bsen6wI%GUHzTLM-RF7jz` zx%Tn&CUi=w#MfrJ{>UZnK!5S`f2lOM)cAwN|2a=q^J*rvD0JB%;4=1H;D4qaNL8@o zRfe}qFKN5vpk51#vkolzXJJC`ppZB5P5FP=541sA@{_TCw=OOXK=BsJWsUg5uKk+| zfj4+{7t1FX$^|ZtlrDsX>%Fh1qg|cp$jg-lehg$D9uoX3^8hB;6*~mYz!WrGtj0@C z<)ooS>6L^1Q$t;~bI=Nn6H?iQSor@O(oz3%e5JL@Euvi@jadLLUjWqXd9fhE(5*4L zi)!^_#3o}`rWQ~@1iC%)QC2vMu2+v0fW^DpC+rpo9E+tLfJf0+RW*$kNT|CtPy+3$mR&ASkxX>Jv>%blHe|M4&-GJ3nfHqG-o9(5NiYF2k&1AG+ znuRGb=lNXcK)Uo41m|A|{!07JIP#SU92&=402*os=qQys^+zkY&_MP9|JER;? zPX}_)o^jZpa`*~5rp3LpUEBjV5yW5x5W=v%GUXH3MWX;I`4Nq{G0zs`8gvCihJx7n zKbX-9>g8MApr@}?pq-dG&ajNJl!S6l-z z$;F|<3}Es!UJS|0Fd(6>hqi8iVT!gNV>jjwqlK8S+yB=v5J0i*-GBo~s#>8Ib+=C` zFr92k5QBArCgo`zFeVb@?!AB;Y84rB*4jq_?(k=i29%qXTtC#CEQ{M|tmfN7r+0n> z0v1L|3Fsb%dwM2l^;2yvY?LL7VH+HMI)fYyg?!Wb#Po}>dmB|M+u&XUIDY%LncYfE z$*)eoYPgK6qVGcVv&3aIwD@1jzV027wLnIgD&ZGc2s_7s<;z5dLBsqjx1er?C--{E zcE^W}VDK>A)oHz7r}vmE08jfXDnp!n{?cKq{!X-ku0Xn&P#p#RSWd z7t{7fcS!h3dyBNaHNFtlCK5e9Z$do?*5xJysK>$0e`yH!mcTR zf1I$OG${X&#C}N8zXk9uiri=ZPFN>KEe1UNvUv>lXn%;f@JEZ^)PK}VO+twU9A63c zLMqYIhdmBQKkFcBFaK6E&g${e*A$*{Zda+s(3R32zSZ3d-jARTLjhlVL>*ktD(QVs zLTPh~cgv96FhlT%fS#Pt>29;IBR)+E(BG62Puj9qT87OSfG_95a0FU5&3_wpH$@$5N>A&3jO=M?7Z-XAX{k%>lVF4ywgmcXkxr~-qninB8!axKE z^g(sciXwwnXPzHHftkF}2}(kF!DEaUA;GJfn$Y~6ZJSw*V+mGkvlqW+ z1k6`V`78Luf4im53WLp4HM2i%UeMB$aKZKkF4%*Mp?%ojBgP;O-%lN<{;cZ+k-(+d z37FZf9%{wdx4%96zVKnUf_K5{`KEtzLu&Hv%4;(AEMu7 zl1*QX7b$i~)zK#X-A6v14-rkSfCY0VeLpB79y=vv*Y7U)5P*&arBP^}M>5vWxJ+9km--g5736V4FJ%e(gnbg4iZbP1gGEk&NDO&#RYXz7G3_P z8&6HPY*`zB9?5X~K$^K>!+aj$kvhy1z@Jtp8&8km+?IO;Q1C!W;@8S?<7eQ(*AsYu z!?Be=&gexkr{y>&WXpF{AgRqco_)YX#GXdQ++WG=F5Z|3@J4@ne9%q+P12SOm`LNi zW}b0{9JL|VkB1WgY8=C{tejc-9{?@Io&bNj|B~7WwEt)CkHl@<5n}p!j9?}~Oy9m&6w)h8X|e$K`Ap065IDg0 zDS0YZK^BB=THk}ofDAl7=b@A^ve#RI?(uU~2KoZ(!QjcIqkB(Cg*)rnD+hVj^ z2WXki-lr|I6iW4N_hKesF!o&>z%!=?4+jhO6aC4OzzH!=~@ zWva@0nz1E}vBl(=)JZN1O@4e_vUackGt?fmtdLI+;_ zYlTg5HRzgxw-@4hjeG9POZE1cghd7=gvMHyM-y|JDA@7LJP6?j)ckP%PYTHb*nOH0 zsEx$82@gOU#x4{ROT?!TXSP0fgvw*W3+;9i0JEys@3_;0CrAXOQU!t{e z2nf<5-Aah)`^@kA`(5vK`GpxlPH#IbA>j#qGrJ_!zX#zWSz6yHplNKyAT=U3f-tb`i1>jvb3FZKUjlJ|s;U}1kcLovI z-N^S7!^6)eE{tHU!y5{DZO3^KLMuMs0y-IhwRcwup(HDH94V(Zd;8PkOS{Oc^7ON1 zJe)Y1e)(ZmEh#Dg&=@ELE{x@-GLD;+w1_zn|6-hLla-Qne+%bJ0PyJYxIu)%^eMw1qAe*-|x3keWT`CG7RxQA) zCoX@aGUI;+F;% zn4md>Gq3%7zZwpZ@8=YkIz)+OAWmN-&uGeEPIK zV2flN!NPIArtg*fhg~(x0WtsOt zisud|yQxAa)oXmHz*F})^5@JiX%n z%0a0cMj3#3Q~L}(2LAVc<}-x>6p3K8d&PtE1TfU~WN9|>_F#Fl_YvoZgU7JgrJl8S zdo6#@TExGts+_{s>Z8y=Ma}ewrZVOI-$Q}->Ro@>S7gbAqc_@5I9 zYh?Jh0o#|)fp@kkAo{S4-lX#aY9#g^g&}uPJ@ZA~0I_c(RKDC39(=Wr` z--F2+CaUdU(8T&?kRJRQt;)K`8fNHl93M!dl?YU_D-3Sg$${cjcTxoV+_-%P+O9BX zFeBmEW)bXI*ie-K!>sAnX=6W&Z1x>$XZ5anm)d>06^U!?t9o1sr?uCKcg~-bjJ@nJ1+CT&0v>X z8+`dTGDo89fC|6aYTfJ976{+LvqkR7|M@Q7vY){T#)*q=pDYB-68JKN%}E6hCsQIOOc$wAr|2z8Q?odu}(q zskyt^FhO!LyHRfgaOY3=WTWUG9t|d?h~1#OAG88g!$Pkf*M<<%?agE+kF~5wJ&P~j z;CoxFZNc;Y(&kp#?_S=%^g(YmGTGO?jU}JzXD~{c4JKrv_WG*#lp|`{C|QVHY}Q^7 zEZco`Ga%D@6w7%lW0~N+27$cUn@1L=75n`re@1sLqMtSTnD-yVgbaUv4;v5hwN@9X8+KV! zzrTRba@S&Sfk3ttVhC{`3!D_mAE4HM`1%VqucXOlJH-P1_p4C}C#TiIK|$A}9OGQS zmX1~CLp^qDXiZO=ul|ZqY^)H#x_$mOSg9lAFWnM^V|K{k_ZprAAK5_%WL1_U1=0p^ z-3CT@F}hjI>CTgiPMRx@ts{Fe_kjr5xj@Gj`)f4#Dpn0n$U$Hc;n7)HAmGnCR;h~v zh7Cp0_epQWuhyAQ6bD8n(Jku)Qe5<;Yj=GZ{Ps6k%duiEdg2dslJg* zJVAS+U^eArfI~?lsX(3SbcK@zFqOSj*s~ii#n#L3$Adav-QEwoOe$q`gt3lP5_XV^Qa`5l7TK=OwD{D*&dAi(>X}`jc98BoVdu-iZE1+j7!L zKl$Cgcc4z{wp^wH^c!3|XFx+|L#5R)5Bz;zQfojiQw@qR*<*qS5%cK&78^t_!jZ!M zsQ53@R&HM>RO^cRlO&jbjl8-ZpkeN;%j%u=a?67G(EQ37-EUU_{tsT!?*{9cTq??S zxjDE-6GT)4Y`Grz#0H-f_fO?)KT0c*bYBl1o1CSzH{FXWGKzgK8pWIh^x{58k{Iff zy=Y`;+}vNN)w9OXU}Zwn+n-AxHSgR@G>E&^GauB1x3ky$vUE+1ZD1i4lGi2+T(oDsnD4 zEqGO%O}3%b~G~Sa3g7+Br*0T0e{N#Z;DHE5@k|j_H`m z@3_(}j~{F2(@aon6KhYOZ?qKvoR?NB>yMw zY`3LF8$t#Nb+1FUb76=k-N=t&<_&%8@gXsCj zaN-#U z;YG^#&X9)1mu`UBoNNx$ZphX8!&}?q9#4C)=>F#2^L6!xtKNG7=WYuTwS7EqPaH|J zcLCzszARabJegYSCcL%CrQ-beR*5NWA;gExnc3C~R$FUP!_x@{+>YN+nu(}iTKTuT#(`M=r2{hQWH>Zb8a~u zbo_l6N*BIwJ#8r|k~Q^efijHGeZA9@*D;~25#!wW5xQq79rzI$eD5%vUKAVH?x}3> z+l-vrJSl5CTK39Qpov#Ws6jT>^_M7F-}}9I)#3ASMj%lDqu1WY^YiyHs~i@qcH9sb z4i<|1(px!hA-*uYFKj%)wauYMtdZY0jM6FxtW}jg^vZyql+QY- z!+krE4Nn%aiG1T1LNZ3!3r}Z%0@nF#&6|T_rK{u1%?GmtW@;@cD;q*((Fd= zLhNAG5ClVEq%uUyk%gU}!tNJ+ka`C^Hle$aR0_|DO3omTKT$MQF$-=3!G;K7?pj zz+!3dxg9*25#IG;r1hhRmZ)g4L=WAxiDu-dHjhDg?Rw51HtffR@DHn?@0!f8YA8Y| z%{G#FLt_aS2`|MDh%80&H!Kw(s9=&8pLVb>ysb0E?kbTA0pQV~8pbi3NVgdzWVI&n z0}%0^Pd}Xj%=kOMc?R@=Qsm5b;*CH36Av7>D;w>A*bURYA#z%wO}G&mf<5_l(W5HY zap=rDfII`e*ViuZjE}*T~y{6;7Z~mvkx+1I>Jb^pX8?nQ$n3njqc~A_nu0H*R=Y^ z6S@YVL2_>Gc8)S zQpe;->t3bWZ|o3dCJGd}3RSzd_%mIfP0hqOpa7hHW=-alQ9JBYE<{uP(- z0%`EQH>2iuh;lkCT&?vIb1tQVnHn0xI&v4Hv(XmHxGRyZVw~6fl0Clwwj0C#5pvgM zyHdLIPMIfV5j(jHGpDu36RMeM07Pa9oXn2c`MqHAI#AKD>8c=*V1+~|t59EbCPmaX zw%tI=_1ic3dIM;OHA*Er&jA6Rd#NArhiL<8p~0o37}cJZ z+?zDi`nVE!XKn6Y%KKe{TYu2#R&jfyFsHGWq^(cIGBL@bnU)FuPtUUe-9qMFf- zqj+MMA%M5Y>hxD}8-R{EF>f}4YF9{+ZNoDNy79+cixxkXF)mtq>ost&;~BnPfkiKA zxc&cwfy*pXytkQ{bRa@+d$4Zwm$x_CMN#={H-$RNRo#cBnw%Av1c!kX$cuZKf~!qi zR)YPK=fJt|cbnCGeU?MbCTn)lkL$>?H(Y6#0Gtt@`nHOa{f4s7X$7YEc|GbJNUovXC1+S8QW_KW^8haeYdDx!j-)fo;JeA3~chu zrYt~zvA)F_P%>Tc;`gn!wRb@#bHCu!0+#lBe`uf96O_}-zd?~o%ZL1q%jeTHeufm7 z*9eMB>eNW9zR+B_P~hO3z@*9^p6e8J48$QA3)#26@={PaDd>irZy!sBpw6Yi!j2+V z8`0?o_Ne`&9tO@}o51IE_zxo~e-ceeIjJaqm1 zdQe8`J0Saywf~(^_|}Mf{~HjZW@%Nzb6+fiQ20FsEqM{OdXfi(uah%f0{j%5Bj`^Bs}To9x+6)J zK1+I5reTU<9ooSTu4SSNa)|_9HsjL{ist?aLN9(u@(_2t5O8%!%91rK#KcU$m7eqQ zD_C_h(fl=}al&tb2>uUW`vYl_<`Tci85k^prLa*Fj6H%bSb)}V0$EOfy|812diudt zSvZwn3T0*+-Ed%g9aH#j*71uITmbj%bMpZ1`3^M0FmCRqP;ON9iz3gMm7?>bFYiNm z-VYwMkqSB!w4C3Nq;BUvpAtm+4Ix`ETpFg5i{WWb|5qt$fjTv8;(z4)nEV->rpIqDE?o>YGr|z zK6@Q)|7QH zTCmKjagDf{-We<7o23%tqRz8@2~PY}9WDentyY1eRis!4EKfIg{(E~DD5Fn=Sup9w zcp@1bLqUatr5PU$Llg%v_us|CA~R*^-+$Gwu~pLbn1X)HxN`(7 z7a@iCSiBJ<>Qeiy6ssGqVsSjF1uWwl_{cFQTiJhh0+4T^QgA$$r{Cy0S52h**!!Ew z<%;!fHUes#{;wEocX0c+bnPEpx4#u$K)Oq5-Osc1&J!=i3Hnh6iMc#oL2%S_$9nWn zYp+9AgbG{;g?bkzWd+PdC)iM&@*nD=izA{|dHe9$yN*O@_{MKDc+xA~xh<^Z8{G&b zn|h+zbM46xsR$U0$8;S=_v()bM8F;^DqrDhVD_jyq^ds2Ht+s_KXndTIA#2Wmpz5Z zVh-*ck(4_r-BiT=!V0t~s3Dl# z=(%18!?z-!_EN^1F`{w^HY0NxGC#y+gnr1@|I-5KDkL~<9c2I;^Xgng{yTL(h|Wz_ zuL?aMo+wI_%EI-joA4w=`|~8Li91F1sICr11_N=dC45=l%YtpkeLt$~(|hnmNwB@h z6hwgH06QO*IljFuC>9^I!wpI2$lOs%AN1gO_|GlM`jVh8q-(wIEL3_lkofDH`lLnM zp)IRIMo-&5a9sNX&+qOM`Bc=B%v9`Au3p{^Z4$w<;=G#Q1GkFCnZ|&OeH|v0YA?*u zim|%U^&WUD(=BlP;fxhe{Rj~@9-UPlus}&FWM&vEN3+9*1bGmG_%JYQkyxE-rc?rsm!`|WP@3l(thK;& zq`(nfjCLXY?}*k3-gY(X-FJi(l-~K;<3PGV1xaJKqbj)scFM2F{Z~#0a_PC7HR`~b zT0{3_q}`-_lXGDg5)AODC%0w=A1i2oqVVgYh=z^T@o5JVVn*f%XrZs6E4fOLJ1u+7 z-pnn-P^om-Udh2%^DNr1d5YhpZ(QZ#%p!zdTLLrVXG4TyV%4tAb2dKnnB_ia-hYh{ zS%3KH`~eR9&@d5wEO;D*Up)fGn?u8C_+woGwcm-i{UQoMvlxQ`svV#t?+lc;qyM&R zqeUf6X#|b82$CouHf-%hn~1B^-oSlLej4)v0Nv^^qPEM~moqRh7Pm~?D9#ytRqw-* zrrR>$s}zL|d#MOtg+)ktWkG(mP;De60Rm%|6$6YVb1#jpQWCV)im*#lgW~gB--psS zZHP#l9<==&Cvqk{wGk9yB;%Mw(YT^~fen=}8b_i8tpruw-s~KU|J;czg%YJnVF?X= zp>rX_1}g;}8?k4`hl~E^lKG@_{B9puI#?uxxd^CO0x|%oZ@=fcjAFZza2NSOA|XbM zH1n8`_%p?)+l^`l!8!^5UOv+qctvH!VxoWN-T*DByynrY{VYHNm>wa}8_j~7C(IW{x!+;|MSgetvjdT5yBK{xofhRQ5EF@ZKXBb*G* z=$XvEEebd){sA2hEWuKW*`?lkZ=IT7NYJCdI;#RzKp{LE$gb&0mXa z7tNW%QSW}qk54@@y#Lp$J?F(Jzr1%dntx5(NLEwIDMRD-i#qxvXHNHTO@ma@R*3k} zyXZk8yX=z-m)Iz=cp)Cc&{ida*#)Z8h7nSsV~`518)wv_I;}kGsjU3V{cU3>?}QvH zFLzMo6(7oF%7be207LM4v1(kA3gx;m2P$9SLzY{0AV~ZYEOTQ3er16yqfO)G40nVW zF37$+UPhPgg;BzqjMlD*7Jdm#8A5fxC|sW2`evm~(yDmcITvr5a8%IoFlnOb z!Jp)lWY^|MaAKlKM3BqDuz*%geYOmABSJqgNus;9^g4Cf5Hgw*x5`XR{IB4)4lVq# zcF_uJ|3Mm)l5WV@`ah4~ewJT}hE z6iL9B37aiE_%k44E%T|j@f~3&P!Ag#nRq2siV|EktWGs>Pf}0`C8;Hq%OXwr*zfl{ z9!oWA zMl&b8=ByYqU{vTd;`$~6)WD?>Q5*BrhU=PH^Z$G*KNb0y8?ww|I*b}-r}3z#`Y@ll zN(+CUwz}uNUuu?nXK;&gr7r!=i_#;)*~Iz6>Ae0T%r~~kFPJ0EJ?1=`8K^<9C4YHB zwOtHX3M@*g+u)vlka$9WS+^Ei8~;dSzimXllkl&msT4`0=)JRma* zauvKzber1&tyScyMaj60^b1BxjY|QaT;vHZwQ3rLF?$Cc1;2bIf&f1->+A$CZ+jI$ zvhCjf{Jf5gJ6#M*jvB+%L+X@{DP1V;W;`aPOCluvC`9TNY=Qn&rXOOADi$#om!>88 zSPrxH`WYZlLw(s z3fhNa+0VNkX+*x_cbQ2E_Y0`krmV%A4>M#4z)0?Q*(+p;lWpQj@%~2Cmnh6}20~dJ z5vcBUt->9wU0^l1@(M&k?e*-7z^r#vf5);5nR7IQmlY(*&*^<2WKpHyuj5b)^izd4%bc zy2VTXwxaa;xGBnk2>~4`+mD_(J*V!AHK#XA7NBK(vLL^y2U?C*YGXp+%}5}nstxKu z2fXd5W^&?ISMO61RB%r2$NH=rsv62_=|`bb+|Y5xBC2o_Il@t+Wr>J`cVORd`JR{? z3wGd@uY)Q8#}na-NuyL5B2@4y8S!_Z=Kc)yDv~^>$1=WOo@5jyr!>CAW}Qu~AGG`8 zI>~I;o(w@=d+wMn5`-Z*6d}0g9f$}xHrq8idjD%f6@UKYW(i?cGwjk zA~gwa{Q56s<`05xB>1|>W_nV@i}3VrwX-*i-gxjB`80zggF7P+?2P#3A_>ky57cP} z+@=q0mQ26*N*Dkb&LcUNU!mZ2A7uLv%1!--c_Wb8u(xWIejXHSVkvH)vX2GSk>(rJ z{pJ-RK}nls1x{L9hUpv-eojz3U7;`e22f&kED^P(VN;Rwr;Cd~7iSM%(^n>GGM&7u zn@b{n^(yb?_uwAIi_DI6l4j+ghR7|(vtK7z#JSPh3S2)zpYB2zR39RJoh=GeAMsj0 zjZN@ol-ysr%==J;`Gm2ja^QN?%+7vyDz%TfN`S^xOjt+gQ%A?y7WENM5kUwq&@Ng2#yVf^%4#6dxQUE+6o%h5Vo`GJfMR6}U&B%!>6@J``^`RVB?%j|1?v*F(P(u%=38w&Cp{zZO2klv5S{kQ*}Q90VgoUS8lKgzb-D8o z!h+8BWaxJKrWpGNhxgbzr3Sf7257>adYf6LVl8e+JF+~XqsDV5tDm29`#ld7>BzXgbQVgGJpW6!Ut;PtFC!G>a`s)b z*Tg#6>iU?DAei21t{grDMgDo2=8(!>3InTn-LJnk%XYihv%=_3%};-Tp+PrP@YJ2T z%^_c>FsJb1?^H0j4bzIaDe=Mg7LIhFBFVfMg>k!Nv3xp)QxuBtwSQoo>el7z z-(nEWF)XS!V0obAO6`{g!zA>9@%Fi^e|07^vLHoYy`UJjgLN}MYS#S|MI@Rq+e+sm zT?Gu@?!_POBj6kX(66GrA?lje7vl>Vs!m z>vaCWF{3iqDG3(973Qm(ZPU=&-||LAoPy$+ylERK<5MmdR(}G| zETkWwe;vS*XGH7e1XKd5)I6Wf@_KkwiZO}U`Y&etUQ<;v%2Sc@378;B*@|`d&=BY6 z48&13S+>XQlD07!1y4q`i{&#fLE8kd;pAfqf?&0xnA-?d5Dx){#Ju0(05`%8qLX_n zl9MgF!gchleA)2DD%hg6HGcy40kCF#ak@qZmr@wNtE_ zt%)lvX-y?mR_G?W_0YDPW2e)(njeB>HJuZ5*p9I@aM%*=K*NO;laZtmd!r@AScMtj`aHU}X|}#DLBOU+v+1uK@M?6&h;NWMu1xZCs&Hkx#UgG^ zbJT%T9pLwGhf+e|YT3d83ghqXiHZ4m*vc$JLgUJ8e%tXYt$VYZ^DpsBkbrQc#c+8> zw~%F^Kb2oL6zrb?n9hx*1H(jSw|yR|jOYFpz1EnINctRy=g9yT#j2qXaTaf@xtI1b zu#bd)u(*0$Y_f}4a8t(8RT=rPBut5Kbjk%JICZe8D*bP_gAlT`5}U@iN^f#M1mTAY z0z)Rjk=hTo&(%E!F9eG&Qv|TEA^m6&0JoD0YhnozfsB}k&G~lQ%*XIP+-s7E^fIeM zcp#Ht010K=!8s{!>Z#&|h=Vz*WRqDw z*F$-wjZh#4^WJ&~R1aZs3Pk%sdS;hq#j2Fa`1>n&5K4znu&kLjY$2gXpM!{o3jq9A zdBVj+2t3k{xjmr~_qc7|+tk8^$k36BWB^2O_s&P=%YnpQ#JCsac64dJ9f=>4Yt~5) zd^AuO)oG!(-4$!rv9^weZq57nk8V%K2AYGePgbQXV1ad!K})hO3qepI^~RX8&%y*L z&AawAxO=dOkt9rPW~nFmk)ol#x;N>rerzE2Zs zQ~atB3q8r$A6TM}wSkFU<3vZrM21O)KoM*>H+bVyl}6c@0m7)@Jok?BxDcre1czvv z&)o-J-)97@llC!nzJHrQ*hbQ968GUM-%i%Bh;?&n{h18QG#5t-t^i;7 zp!@u-Ir2*!wT_nb$9i<&1L&b1tVOi~E?D8~lwj^Wft(XmsK|$<Hr~6#Fx8Kno=Usmb2D zO|it+gcP!BN%*l^G_ARFF*ZQ#>4*c9SjP<5+D7hRjcza`d<=b31smZAk$yx^+f~mg z#(OcR`vw-j?n0XwB87y3<`Mc^3?bbB%_D%L#D$2vcxJ-SSc$Fer#NXa#)9rNuJ_tR z6ozG)#29xW#LZE7UiW#0IgtqDSP-X-T^b!R6gE)LuIk{j+X5Hr$`;2rgFZB&T84av zeF`c=TJ4ySC_RIE!|Oy=;e{u0cSu4(Sgx9PfD^DYK#=8>Su8+ z&yiRNLzlRn_nonn#%@x+EE4XI%RLm|^vA*`Smjky8|*OszMBO!)_>$0v{gLApw zv57>xr`a4`SMu#;16AzuIN$sV<35?ktQxmK`B2(YLBt_3wQo4Ys1wO0*g8HC+-XY{P^*#G`8#8HUaA*^))iVzTs__^@aISAA- zh(2X)RI<>;ELuYbuO1XVZ9)HD{z`-1O*1R0t~m>P-0h7#kHOUqhdETit(>h`^Y=vc@2kw z3u#~f(UL&u`v^av;s$=sj6e)cc-T;?a_u-zsW$h8rp6<=OhX9%D=Z!O+__g+A*sMm zq7zt4P)4{!_%wvsN!2!SfZgnR1tQ=TPQT#biP87>Y|__|`HZK9I(Vs7u))Q@Ho(0_ z165veSK?~nMWP-H!%wk|#!EYj*JkoA(wDM}#`wCZ%Y^p|&)KM2q{}OT@Iyqr8Pid8 z5S)A^+%Xz1=(e`vwyxS@@PHL#OlWutngoI55MvF|6SC=I6S))?_qO4a?J%X^AYboT zki`T)YZ=6L(Uu#W{#|0vtM^&c@{?b-+={dn03}j7f2o@S1Bk@hUzBI1fvn-%k6qg0 z>*|rwFzw^(TE77Tp@eI=DZ0T)Pbf_&HN~EBo~m4Yf(UCeBa9CcJhBS7V5$~I0{TH{ zhzM+9->+xnY&MPqf%zmagM(b(PN5=pQB0w7pVbHDH!jG;vqxj9K(X_s_3DD-Na&fy z>k)YiHmN_tna|dK#=NLvUClLmd;ZVQjfhTscA7Afz=6^psts!Vkr6E{0;=Cw^ML`8 z8bv4%3S2B@%qANA>l{j@121W(tQm?Rult8^mg4APJDMRUIafCL6T4iH=x@lk1|Cd# z3Pt9HY;EZu=O%#!lqCRQ5ShSU`Y3L;xEIXIlxFrO4cevSpa0J^MtPi6%83lI(Xuz+ z%6M>%fiL#jvAoFj8dO1iumGhL9`ug36jD0;m&wW_T|ufwcwH`79-<=G&=7%*6rJ$I zp=%7_1g`zpm&WiAlPo>0Ws@{1#H;wy$M0B{MO~yd4=Csdl?#b4_;o@L$%4Zd(Jn4I z97|4t9=`pgghF?Lb%Y&amdHLB6PNxRCWP)%X+82T5;N6R32AiIUSkS#- zN0bX-3|?}a}q z>!cGrw!}*C5-Xq?n!fx)VTLd*ks5L%ezb4@r`_~4HJ)`g?Ks^schEGhA9!Jx!NWJd?{zg(4P{$%}eKCvHTBH=|~~^x`(iAkGzL;Yb&3 zpDz`o41Qhp*9fgC!h9-$QxmG3Y6ZRpmn=%10NxUsm}ix&vgZvPLzP_=_ZE{LQmT2G z{+NTxG^U=JyM-Y>FSG=vBh%W5I&8lB+neRv!2?!cr!ZI&CmAbFSysvEyPq}=}P%#pu7Z(&H(cJuI)eMS+9na!eOpn zN&Q(cCUr^4i?4&_Kp9U@l&Qu@s6$KJt`iWRA82=v@N>|Zh<@*vqI>LFS);(|UT=_V z_hk-RYYbWdzA!SW)_P8%J1z;yJyAB{gj=te$!YEp%Q-;xO8O6IZc`E~$PBRjaNZ$; zilPUI%AJeBvkO6}LYKYI$r_cc()?>mPN9W+mbxB$!HYNfr-cc56kRakHYNXT5|U2x zTuT)pvojjwf#?m%$N@tePSWFe79Qef;^hD-77cZTEL^p{-MJ z18KTpT3*ad2`yP27qRu$eT^^Vd_LR3*f~rw8KIaU1r6pfl_6_Exp%pS%rkq3S+b7Y zhu|-w8w*uNoud5ULVPO}BG+Y&;;|rxqnQ{9AzX$9>y@>D%eud$Lg7DnTXDHrRB)Ke zg2Gmq82RI{!^=C0u3VfZG&Su-rxElL+SIrZv|%yze=bs*|Kh6Fx(n=|=S`6emERB| zvz)>8cD66BmW^bf@;j0x%AtN;bg{EOMeJUUvL864ba+x7^WUA0r|;*{NBwMq`8nHa z2q~%7y(NvrTyC27-{PpQu3*i{Tr;kX6etLUl$o}P;Q!qq=3NNpkD!X=-uh0Soy1rf zIT1p1%JBW^b#XcR{Op7U8iJ+c38>zYe9`9Ro0IgVoAZUNM#){&CPBOqB+A!K%7P9O zE|>JFk_C%DE%3Rn#ib6hJw1W z2C9Ja3H9^G5pgG@bFWwcB+BUjm+}fgPtc1iF_T2T`r%z6MJlD-Oz{-uPZlRQiYggu ze<;NH4O~!=!KsLRsJAS-t>seU!bxEZlxJvv&1>U3xqfIZnlo=V2-J@&B8i^6^~rAW+GwVRe9eCQ6~XG-r0@vFDEU`hJi`PHAg^ak)N zEK|#nWhd6ZR2BO47YDAa@cvpRtU3|IlcVnrdVM{vJX~0X&5HaqHP)P$AONbe`xXvJ z9b~<-(UK@(DD4WtX6L+&;6!+|v2;wmSVro_LIY)R!wo~Jfk`QrYAy`m1#ej`~cuW;F<<>@I@)TBJb!+3BoZs(rni zWBnH(#fE)C>4aba`<9$CxLt%?YC0 zs?nNaD)^Topnu9|u|dxmg84Z>+o=py2HI44qHz&dxTs9|P}RSdyaoTgw)X%oCFSm# zC3y#43w-RNPEuss^a;8*E_J>2f$WXDKs2bU`@I$F@r&F9tHKyIh(fF3kEAElhJUyM zwF-4Uj&ix!(0ev5JQF0&zkOO32S6K+VqvDhuH>hvbL2k+WJYxV%L$C7gFQrOxgkg9 z2@5Fiiekp7tSEX|!LAGc6Pa2z^7<6{QloCSD?P^}2=lezz|=cF|D5(3EAiuKe8XB7 zUx`paUURY#ql5T=ANx^60sK6@F0IMjeW53AJ(OnGGWC*O&hNd!b&vK^R#9%B@Z{UIR`lr0d|E^RXu6jZP5_j=4M^XI%X%OAv-O87r zJ7rvpxq)Xbw(H_DWW~UUNumL;B1x=!2!K_f<>7Emksd#XTXPS=r$|9#;8TuPTes-m zILzzG(I>-1W2B})Ug%-9%;~$<#yn3RNGFF$hW!B1L?OzL(4br*^|7X6r7+^s>8r=K z=4b9$Z^S?P7pBrcm|8lH6HvAbCd#oZ8C$dUVS|CLcwgkTq?Qyu5l82wnPd5tACIOc zPXs`!l6EUJ1(r)u?w?)LgZ09)eQr9xu@jc}aG8i6P6$bIEa^loIijZJWDMdkndMiiC_C? zQeRttk=te}QCrGb@zexiDtbX{66EKg)=8J(d!8>G{W4?!Hcs61FuP3{Wiy>|8_4jKjBNZQb?;a=<=y=B$U;*L6Ps z$V?OFgzj*D9X9%J&@u-|kV?hWee>0B8Q72roNEocFT!*=wopaO<$fSS zR-7H#iZDlbsnWd_K-X)N6KZGDXf6^~FtGw*S_;#xz7^HR1nyKO#|`fra`J?iCTHpl z5q+_MEwn39v?)S7QUFu79YptQK+gnWJb(m2pyu~uXYmk>SRa|ZJTx3As)NZ38e(WY z*iUuRLfFNjPg8P6K@Ix{Tkx%WP;j~^7LNk@mM`eg8S$Ij54UOkLXhPLe(sh%$ed|D zET#FLQl(eXjT`kdfdse-_nUN(X9)q=>W7H<)ys{>qTf|0`L_v^qqN2>`=H}@>pg`v zd+XX4fM5`Q2e7GKr@7krc?ZyAVsZ@7de8Ob%#3D^1UuSTw_LDgN? zhTb)MTrUPaH%o}~#(LM`?eW= zolpBnl$+kV^dVQ1U}qC+P5q2t5h&?foP+DYmY9jcb! zR;zP+msX7rV%EE>IUo+~t-Hd;LVOe!qXXCUZVsJRd}(l5&l{>>N)5p@>NlDQgYpOw zgeaKy5*XR&!fk*4TMSde7lk@+?^F%M-g#?OLWQCzr1vANgY3pXk+#CKfx-Aits()o z-zkgQ&RtTzHn3V(oVI2ZvD2%7TrIbq4?~*Le2;Xl>6-B=tE|@e5s`B_{~f5>t2Nry zW8!Blivx@|jE1A1-51`72a`{N>8zQA&;v;t6Jj)s*fP1KNXzP&WVMBQkoe!L`-XQp z=Gw@r>`D{+x}l!BuAYW&MhH;bCBvvVOj*e;dUZ%A!{Vfg7FR^x(BEb6NK*IKn&l4W zEaXj<^WDX)Tu8Xz<@&3l`^M95=6k<)8|U8fwMpEdB9=5QJkkE5f6JUJFwdABm9LZL zauvsln|-dd&%jFzO==gs)A+;FYl-8H(}R)<`FjsT1Yc4aa{Cc6VPLM+ZGE1_&`gpx zq?8wZ#~BBCDo>c5WpzsgG{$>=*?H*AU)=HGn|mU2clK`VCe;5W4_BqlrN|s@>ByOg zlF#mH71jE@9Th|uV)416>_jbvu?S?4M#r!b$28@tpUegF;UuDSqzB&XbBCBmp-=q^ zMts2Xz}sDZfGU?0{C2ZJ)pTS>@qYE%v;Egm>`^YSUD6{#F%7<#I#ef!wP=aXm~s4j zrUj*ItK7lyS+Qb-!SmwWJ`pvsE%R{@9duk;C6}+%g6Olju!7RA+J~&V>QJI=Qj6Ze zCDI_#4qEHG04ZXi_Ii=Pif{g5|3)sWIT;8o-Tc`jx!dS8ytb)BsklBN>On^357SX; z^`57&8c5RIVwiW!a7d^WeO(pTN`;3Rxa(-^POjd^pn~bJ_#*Y@xa-Ie495jsCsrcj zUwFa7ps z_Pt)8jCN75(QeDbC&II5F4H zM$-zWUs>V&N5y>aw~I7A4nRczy`k^(#&ue$+r74nKRc32+UVaF7fxe`=q-Fs!L~z=dLKDlw=6IfOtsiMh5E$ z2Sj|{t7^tPtZ2RXjn@J8pK93r-#^Dsj<&u$0b&emlq7is#2WzJsqeq_^78GZeMYrs zx=cLi?2Yq{y>>l?QD(Q+4@mWg*G3bYzkS^_xhKZ_YqZHQm7VxduMPQqKqUQa+cB{# z_`~MCQ|zii-!)mO3=E-YT3rM^n)mTx{uyy&x~B>3pK}2&rQ`wN#hsI3`}u3rJm=5m zCdYHR=cp-6rHrkoD_cUJyq&2|lhP%()uH;yXn)$C`%N{RXt!dnEM(EKJ0dYoyMW^o zZCCMlhn@I@0uLxWzhcf1Xmt|4SIK;;v|%Nop-!NL+B9?N@v}P3#-t1p2vSd)e(DkS zWicl99d5l0>la$_tZD1@#(2_sBlOX3Ii$v&de ze0e!F#v*i;3tCKhlRuKjh7Qhu86C}0?X<-qNPknw2%`XLs+Y?~b?hzPUvhur7WrZ# z6vP)osT4N@8`)kn)7i{GHA@0s>ki<~z5s+4dB=&snOX7XeR7|FtI7ahWB2zjpyA_i zuc{4bcC5Y!D+K2jyju>=eyT%R%H8(O=i^mHkKMS>UeA15GsHrirba73K4ESs-xZ%$d7ziqHaM8jf<6<-lP!DiI)d594h zID9{CveR@t*ZhF;Hep5ZtG!B~{&KpH+>6U{c?@(xo&r9R!{2M~oTG?lUk?zbC11&ZG|dOXmU)c38{^$UkPk#T*5?$4aN?j-NY5g0(Auzw~UX``{@u&q#aHlUD``mC}Kc|KUCMN>c!l2#i zBUu8-l7Tog-)sCmY~mI+XYb_ml-9U#cR`eE+V+kg&m2F?#`&Z3933MF{okMpxTn#| z`As0p^w{NiBK;DZRBg^2=F-kVe?jStx96ax7~Mz1G=B9YL(2fxPYMtmeTWc)RtnLf z&);JoW8cNb{%wms1yi6!6E$ycPoH0VWzbrV<@aRsQH}ABk1GBN;x+p6)9)TF5{DWQx3k=|wmuYWQeRK^{Q0)~rFhUi z5@JbX<0Iio-jmzM-^D>55VQg0w`>}Kwv}V^ZDhb% zq&ZMk2|FL3zTvdTWwvezkl!yv=5_I%5JB(rP4N?G?c!~>9@Hy#58eq*Me5jkMB&mb z>pR+qBaz2%1y1wbz~kYibfNi^YsK|%ci6xvAL5Fep>-TAvB;nT(By82tpNeCac0k* z=WkLyhJc>icGNoxu?K$_ullSFH+H}Y>%Gqi`!LmRtx&y!byQE%$qaVi`}4Hr?m!Dz zSNCDq=g*<{b;_=?FOA|{Q~ws-J{iO;uBUv2N58?TIQZ;bhUi zpV)${87KU5K=}Va5GEA52}aBhVBs^!;Jw|t9L8N*axKEL(Ou=H0pb3-g1bM92z#Vn z5LSljPV{(=IDAT_UZdw1jlW(x`+;cXvrBrGRuucSwWJS?}NfoH_3Pzb!6+I(y@^$|Jm>C|+?01{J zW1q@Dq*uzLD*ks5rwgl#;`?qs&N~+y9j1r75AT()!umrIMzNO6#=hiRv1$fu+|J%n zz3|ot&j&zWMyUYGK%c5r61by~P(~0haT@7-w9AIE;OL>{m@)RA(#1!d1cQu{djxRF zL}i-hzIGTmq^ z?M+WD7eAM1n&*pYpuKvEdXHt8x8x>-!^b=c^nMfiM z=7A08``DR2!CJNxiscEwEIQqUJ?<@C)bh!=_CLN4IIqe5lYWu4DKUUJWqdCARhZU( zI|44wJtRgWoX_-XNN*>7JTm29ITz?=M*=(D-kxGb8wCc9{e5vE<$s%sAX(8uE?NO- z>~2p%aSRO)V?r&n{Tk3hD<_MvxJeG!OU;&UH-v9CdpWf!MuyHMo_3(-DgoxxS}$VLo1eq)f1^7|!_0NmIAo*UK&taJne*214!XVYheN=q`W zl<5U{mU(AfK7E`fD-V=SZL{6y-%Fl-DAjT=T?c8O*T4`znpSsy3G^XT%R#gKt1Y?ZGvJJE#x`sC@jd(8Or0Spe}s4DV{!=> zQ|-1Rlo2|Z(EQ!XHM}>wTpizQnHaN{2~Y9=9^w1@rbO{Hf2ETGn2(}ta;N2YQG8=g zFU^G+-Rn5S5fn$WM_ zV^-oISHis5)7-z?^G_vLscTGQEij>fZ%1$Mo=QR5{YHHjPhZ^9yqfSl0_=_xM!|@o zlh;>>7zaq&=`L&62EDqmSR3(umr;3Wr%Ujka+a}lw)|m=}0nUfYO z6gyd<%AREjo{6gV;I$vZKyCpz6tBY{o@5{TZq|}$DVj&K4C&~Z`qlaL3*_+aHwq#0 z?7VVB>+`WKNyP*f0OWJ>HdOoUb5!gv?T$aC`-9pu9>bYwA++-L@!EY| zsSEz~;{lZDZ27M2wTq>Ij-Caw8iVA$ESHGNAiq^&EvGvV2VgY*r=)hM(0kma8yu}Z zExqH+W&{Wl({)i%%qDQ>6+avO(2^(*m&7XzmtMnEH@IyGFq+xLIP#R!A=5M5<^`_` z*|}kj&(hbgsv&rx5Ds;y;p26H&7e@5H~um~!#rG0l(QjG_X-;XDrfL(UHLhA6>DCosS)SsAU z-SjMF`BZz7IHw)8KF&g%)vq7i7Pz=|p_4Zv7>LE8r$AlYCe`T7^$sqQ1nDpWxJ3HADod?kt00X z=Xgi2Ww8ODPfYagoJNQt>>wt50+%|6Mf1nHcsN_!a`rUMU_XGsuLC&lDg6&)fh+H= zZWA;Jr7g7eNzT=X)H%lZU(*J&E-4ZsvHb=9!!#NA84l|{H7P8x`yGf-Hb^rf zx9g~e#xjVMwj3;7IpO2Z&Xqj!wp7w%<_Nnrr*8POI54g!h&8~Gqh-r|=PWxSbvG$> zmAzZmkb~3lNM$t3uw>Zti007ieH_681HudIyXYpht);&~mg#B6v==@_bDTeL$Ms;% z@%kv`)=yzO!*1vij?}S#PD?Lwk~!KEGyqxnBeZlIcW^%EV;zxGmG#*E3-#FU_U^-q zG#Lrg!t#v!j)Oz;;KNz7X*WmzpCu;i?bkmm&YyfQEV9`qli=el%JMmAQo&cWPY#9X zVfcWJfSBgzzn4!nED&QO8`pE8juAxx4T7y$t3J*crGY$HxA=HbGsBngH5=3KP=(vZ ziUsjj-Wvilp!aWZ@$7K_iSY%&*ej%Ih)HWdEAanW=vjNGkh%gFz?596H-?!{G%Jm- z+LU*acx`=(aY35{9GWE>U}K9Xe8(7;MsRR*hKk>!McPzfw%K`IJ=V`%pT^sar1ymI&apw31}^!*O? zzv%7&b?|Pm!>HBt+$n~mlO#xD@WMW_OF0$`ldZ_i3vN}E&vkJvVdbL`p zx$2`QpF_LxLK$D|r5V^zT$I{A83ueUk2bQ_Z#0ui8RM_5&r3}2hI}>&$gk~sQbktT z<10xiIJ_)kE3 zeLL>XPv=J1o=&TtN`}}@H5%VZW9W}NKv6q0I*TRd&V6VjGk~%dkoTJ&wSNxTS86vU z^OHB~5BVTrshA$b|CG&UmM;}w;wTW|=6urv=RVo`6nDOkD-ld+CHrYl!-{_Gh_Vc! zW9k$^^!r(jL!W?N?YmM^4sIfrPM3M?r=gtm+B`3}71Z_lV^wGUFo~ycO0lt~HL)U# zs$zS3heLh|RU+aePefctGuZ)Hy;qC&VN*=~0aav-5;`W|q#vTr!u(H&1JIrRAiPcS zlW1xs37;CKjC^)z1;Y0lHQq7xJjQYu*lj9qx))*%6h60(t@ddrQ>)Z`^lP0gcfPr2 zM1|C62TkblAeTT-%Bsn3G!8p2@c2Y_TlBQUBi1zkctWWsLl4Qx@J6CUV)*EJS1$o- zW}N(d?Et^mIOiR$p8a2gUpuGp~-yU{D-y2u#+(oWE^|k?I-tVnmqU|6g zJhO4k6s6}KD9w_i_znEyGkHzN{N7HL8XUwp`#vi20x|*_r?t53l7fFH1tL(?$#>5k z!h|Cw=`DYF7s$}*@=!?KKDlXbGaMRXA6nIV6!CzRvGr-GnI} z>GKv;<0M&2-9q8EZM8*l5byhyOMxw4JhbT2{Uo@9(XZ9^#ry{vN~9*abYmG!wZIiB z9sLj<2=C27m8!svNf8Zf*7V;K>$7=Uy@bo*<8`~TZEHCfvLM_uL~oZ+WTqKgZ%cPb zY>n@8-ji%#32Th|)B0M*_r9^A#H(XsLi%dusnm5srm(ZgUlIwcJ-?c4bJ5T>OkS@a zFMN;t7FGJ2k?sKpcsHh`Ce-;hqNlR5_J3kY{laO@Z`8b?xPgw$IYv%u_N>-B_Xt%| z-5hfz3^}0KjElmGAD{8~6MZU6b;z_9LnNny8|ufvkoS>w^`X-w28hV8_3OI_-#}tB zNs*T2cmC@D_^#J1!PXh6Nxb?8Q4jKbQutz}p3Jkl9V2LnG;2^OJN0W4UW?Pxzq(#zjUbplNY)5dh4Ux5PQ*C|KZld=t%(yOVvDY zL|pnWDtogfL`aF^$)>H9o<9*m9^*Y7VjenO9XtI{_VK&=bz|SydRDo>B`adX)r*Kb zyN+MSO-T`V6D_*O{@iUw*OWEPeEPg$jYQ9hq=g-ukxpp)5A`9dAl7B@1i{{=A_rxX zY)?sYI!n5NQVf;KSM|aI2bD(!j|IcVYE9@++LjI%<2t3?VdG8iP1YRfG|VHwagwL$qaubMM|2IWNsY3{neE0T7w-ZwUWopVm-97;^Vx&R z32PKx_TO{DJjfXB8>I2;gh@I|F{hu04(VmRL<*y@xH0=#I(^rumbP%9y+8ZcW&wPB z^)rmvOoR>AMG;Qf6uh)6h#ZEto}#joyye6we{E4$Fr+5n)Q#Q2kFtT0D@G;sP-}sALOgFJe~08eBl~>KV7APS#=;?yMbrn^-~=#3IhGM z;X6d7XAx8-eVl0x(;<*UP!@PA^))}W2y8+(LAX`jo&C5R4ye@lgdsdY!%Vz9XN%%S zsNwYkjhxhC56pmceCY@V7VIsy@_iVa>qsWS06**bms-N;h`#qInW7i+gM4j{f@;b7 zM)Eb6y%7A;&@nCz2_@=DLo#zB+BZQ$o8|u8W0r!%*Zm;BW*=o64i2?4YD0)9e_i*Lx^R+Cwg2%lnm;Ytj6@zi$W%_(3CTQ(yvydLIW zL>G>I%{Bq0ch>l%Zq+@8V{(K-C+XS_h)KTa=*QtQ-hk<3N|#Uiy89^V{J7p8jvkp# zV{*Gn?&Ensw$}eWBCS8}&S$?)&sTqraSRK^ynv6aZ~&{b-J5GN`Vb?)PSI1@C$8iY zk3UDp0rPup-YHF#ZH1+fs6ex52n(z$UQv9?JFV0RvZ^w1=&e5$u9eL+)4QqN^yJ_o z$}=!Mnt%h4bgVBxg+c7Gw;MA*4pr!utd)&r$Bai~D?m)$vIN^DmCvx{nh7tsCDDyW z@XUgo>+fC*kH+FPfGY0Z+j`LL{2vQo-SnQhU3CtG3ykX6K!s3XrcIy3q%I%WwaX-A z&+vghTZcWoBD0<*5{ll!6gvIJ6x&e=z2QiL*y0)IBya31Du+rL3j#zracLB(L+HqP zHDdgErefkQC={7q1_d?Ijz7@zoA;sUP19xB3+44SCwcegAH(k{-C-%8*_L80DD7Rp zBl?JkdO6O4E7y$ed-jPgo3MT#q%fzq93Kf5kTNW^l@U6m`&{=&cFMP;qi+F{B^cZ6 zXNokjMRhoVRFvhc_u4()1PASNHJbk5p;<(*)tBcYwVQkUoAr*T{V6WlD;Ai{qwyT( zj^PhTNh~i5y}P6>wlMPeEUBk&LLYD~d^lF@lLQt0>tlTrP}pe!6`#fN#&=&&64P3H zVJMxVm^t>PRTw3UTbj|wv7yH3ci)$n07-SZAu)vVz;dD5JC|J#I@kqJj4N$HYz=0> zWJ9S#<yHTS=-*c|Jn*`oR2cC#S?L^lx2s3Ou0XGqn-nK~n9boa+4 zRKv4qWcOpRZk}bjwQdB3p~w)n?;zicrOA+t{9$(ARSuYW#E??rufSC59%5EJfPRMdrylK0Uu?YbT8E&$m3hZxAKB- z-!{_0WO~iM_mm_Y0_)BN&~P1Hr#tkNh@#1P7d;ue0O)(^vUO6$7jcm+tK3V9Hbb9Q zGgD{}2DxC6W4lE4{YS>fQk(%tMp-|`EXN}TJZkQY|HeM@f|e6?3>uK(ul|j;mFVd} zrMv8W!2iUKz!#$JbANl~)Uscn=@AtdaJ%-8N_x{B)X2` zfkj)&979V=c!Q{cQcnfOhU0U8=3X~D0kv+KeIlU$tVB^aq^`kMoElPQcxlD=i9D=< z`k0A3r#{z<-iDxvOQ5QMBHsO=a=;>piI>{`xGuFI{}ezI#_kV9ZozJtPC69rN{r|f zncVH@iFX(NiN0AQm0QOcXBB)CY(s~NA^htkq?x5i&6SQ0Ifv@TiGDTacAtvji@}mD zPHLl}ucK+oFjp5NJFU5J1fp7Yr=@yS)~ywviMPu=qYdkl+``r?z3qCF#T+hN%&8fA zd0TcAuCOJ4gaF^&mBu9p=o-809BwP8ZRrQD$a321Uu@)i5%a8K_p4e|rBCy&iT1@j z_9*wGqnL~JLU6$E7v6%{>DqEg1COujXF2Da2>p;v&UJFIT}rorkpw$Ee-D<&x=6|@ zF1lTqypT8eJOwFrdYGqSm;&-?`Ot^GnIh|b!yH;krFkL8^~h|;&&&-6XV{K>!AE}9 zLV?CxUwM-FTWsViOYRb~#v}3fYV=}vMXLGHcv*x`-%N{x5Pgd^T->B#T*{1^lk)$#?dff(}QBTl*aA_L>=z1IA(og+M! zzLFikR!*>$bqsp>@gaFiJWA{vwlT1}-l^^KOHZXV0(*7Qw-^1AL$LF58qayxJ!L^E z2poFkQFU{&V4-f>at@4jvRt2RcYgBxfkAAO*d6?>)x&1~nK+23C;BT&`Fa8^=!q9BAMTP10mxJusmb$lg-?RNRd$Rr)rmd_9$ZdUh$;&Tln^)?y z1d%qQrCDlGHDZI}_IWE-#Xh{;sejDU={d>~;?3xsGPo?BeNko~Nnn{$uTNh-q!sTA zK&X{!Iq%kd{k_S`aJxK3%CgK|D?Pv0JF+A9Kf`U$ACYUI4%6(Bb}dHQhAxs8b)Ajs4gg1-FdI!>>4$o~pr29tMZ1^;Ud^Kg`D(rB~5|D5mUs zb&h#v9P6`v=4U~1Y9+B5(F(f+(0Cq!kJefhHsAEEa<~u`CEFLKRs$d}F$$nu>xU|^ zXxa3nVY538n}RQKVZP~26k5R>`8*A@OGP+2?x>GRU)4_UaoI4j#bx`kad8b_CF=q! zf<9_DUVFGC@&vtUg>CWOxgRt2sbX>wyot~#`LW;Tf15~iKM;v)18Dx!gRZk|#l1Wi`Jk;2?`cmO3Oaj7 zI9JAo=F5P|6YvX8_@D$I&3JN;=%*Gc8J?KV|scMS3*n4TbeUJAviq`3IpVb zMb2j|??$4$N}-qKDct%46TLX6AuHE{HGF$uIJ3(ByX1{S2j%zq-q^7cu3g%{$8>*J z+QKFaxBY<`z(G%3QS*g3XsF8P9V2=rk~l31NWGyFusFu@E;7Y!(u711Vl=&4NIF?{ zkc}oK>4APRDLND^QysAg9A5~RP|cK_y*4cRb6iix+HEnPCd0i~lP!2kH2OUgw2JBW z3~aL>zdQm=k5?*zk$E~8r|k?$&(UO6b*VGCdZXvG;#&orQ_Wk^*V*CsXQ%VO4mnV8 zJIEDAhNzC4BsOo$hldzCFBMnzo<@Iq=c%0N)uk^G9Tj5jT|A>2g-G$W9@(u&0v5l* z_o75o)W)DINYEO3>17763 z%XcSnmhr~6k>b|%fR6syt4X6(yWzI$_GuI({bnS0l;5H`ark??DEELkc_U?5CKD;} zPc4s1vfR;G->U1uND0@zVfo-`>R{$V1QY=t`o9nQdPeK27M!0JQJByd_GOy2vN3(7 z03Uk`4}ox^ZIG^(uE*-8vgn)8IZrrV+0=xPsF8D(R$A3iFc}Y*D$hW3gp^iNnqXyx z))RZr^;tTPZmqDW+#kuls97Br|8^)#9177-+h>0U|Kx>Z7$v##Rf_;yCC2k>PED(8 zRH{i`Z7bnH&7amKXK~o@I409w>L?&{`wZ>@o@sHAQJPh|jNPQ(l1tCSyWO=eKS!JD z((-2;>xmoOJ@B{s;F5P#_)QiIJ?~pzqyh0oG6LG6L{F-Eg*HTDrW%dOd0H?4AuQbd zvgfGEh-;pNfgNm+Lh}97N7-iX{k@Ww0`QqFnqGF3`<@j;Z_kRPxU-T!mUm4(w7c1F z;H9pAP7>5il(#CPx?aVzy>gTKC&s>+fcOn`6SV)ulSnHM7xRD(trA}Vbs9!? z?XM*G7m8_rYzc2&D;BgD!D^+EMPM4WV`r^bhXutOZSO{@prV-j(08f2-PMuUV&X(_ zR4abB8zW4n72SNs4b~;-^l5MNqI2kBFgA348f{DjPtA{f&8^m*D&buOdZj)R)}%uY zSMFsmYxlxG-;t&7I|sd3)20D!QoMuWa~h5{OaTd3yK{Y8+=aKIi82aZbj|2)RM^kM6?^D zh{4;*@Af_STriml;pm1uznh6>1xN#rH?}6m3QhNk&5JWug}>GQw#?n~m9OdtaaDfb zfkDTuRf9OLFSrNx3x6$~$#)f#bz(bF`!F6Xg2-&(F}i=KQNS9ldls_SB~wn5{6xgxojFB5e`xK)Zz;I5?QO@y%LUIcM&i6gk>6-1#oC%c?0U0VNA35OEdCSF!!3q46E z9M-a=e#5zwNYY;hAOOS`ClWE;mAj3Sz^i9q+4~>dku=1O`H!pzmdK1PRNrJ&5 zzx(ezkqBwkPI+ApU&mMOn=QG29v)^85O20P;_q<31YTrIlH%`utm=-8s56>tZNo25 z4{&!C6b)DgG1yy}j4S;9(02-_^7nz{M(B~OQ2ZfSV&OITerFW9&#z$~0`&b5mKBn>GHSOLsL=*b%uH~@3<1a&WZ;-$)UjJB((^iJ3!NA8GgrChE=;$oif-;<)ab>#uH?D)kUXpV+TOoueljR;;v)kln z(zwuu?V8+uNUuI! z0rMWxdNJ>pamSkFf`Y>v8=~bP@QBH>7iviye7d@Sa(~gWq*0#@kU(_F(-`O*Z_oQ| za-eSOEdJ39J#Zo5ney5h)DLfpc(=SEr#=WJ9E2A{6l?1xV{b6k%?5WVlQzS0JaFt2 zRC=XU+&ZMiVn2pMRe z8GR5At!vL!gsBih@PpR-iA!ZkgM3kR)xV1+(IuHL?36(T6S`r%i*2TZ#Rt!T9%#sl zx&F5$35IIOAhCf69`B3Frg9Y_=S7Lb)F}IM1Rp)nbJ}ouKik$1o-=_2AO* z#&2BJf-k*bPNA7fuusdy?hA&I0CHFfo4r+pk2ix+`lIbup4K#_?SoZGjJbjmI^%3m zk9sbZNx0yDmng5F%B(VW6_f33TbjegLKLTDQ4?@^XZV;NO6o^v& zr{lS}D+vf4NofnzQ^*?T?=UG?T2;SN^{+l#srV|wr)xLJ@yrnJXs8JlTZ{IXxY^n6*}6-7wWvlDXotB-Jg$qON%~1Y_!tv9g8*}< ztw`BhAuJEN7X+q8?^p2e+J(VHn?UR@yb${S{FQ9;XsZ4ygv1s1IT%L`7MLV`M8u{E zErcfj2~HwMCnjQBTV&FCyZWZ?Ns_eD6ZEU>n%A-vD7e45TH&kmt*&d&Ubc{-!HuJX z{E~3H5H6wa$3K3!S$5NcqcDH?RU+UQc$FTV$%NaF>v5p@w{l}>g&f< z0rsowwO-KgxNGyJz`XJR_@crIFGO%1L7R{Zq3AE*s?|kC#_sjD>{L&O6m;96GhlCh zs&|M93E@%F=l$jm^-@8(^10D)4h zeSla{p%->xk=X{}B1K8^$zrm`^UBu)BP~t3apTF!Ci_&eouI<~V2LWzEiw(_C7MJp^Fl0l+N!3fiAoSlA-BIFcZaBwvm&_0LY z<8OdtLRo5f0*oY^NGUvK+y<3Zv!Z51weQZ|2+@AI-n9VL3N<~hR3O(1Vh2aW850l~qrqF>4YAQe znb1}`<#P-hksfucDmc6{NcyY5!7WdkN}{he^F`^5^daGv1oEQFB5ZNJnj=vy%@s=`^HRL6j-Bw4UlI59tC%>=AF7D2Qy2zmDgWY z4x`eE#DU02TB+;XL6)aT_i4r+27gBl->{NDTJY9HyG)(Es2z#?oj;*9<+q>N&;O07 zH$TDd!rxwYLg9OVx0;(uGtbx^vC^#&<+q!-;sV3}g9zu}<8Adk(wljBb zKac$!xF|Ba#U#HA1@B6Ny1@O|8{v7ES@sGLsox2WzE1qwBhz12Rh|Eki5jCC%@P!Moq%w)}c#u24!{wz|-0G$zA)+2Y8 z#1{}Ih>thHtiuvAXC2wgweOAk7 zQunrS4zrQL?qj86=nL;ICff|1+L}d*cJs>Qy4*nT)!8>19+8vs@L55Olx(w5J#m!i zho|M9ZJc~2f;oJ_;%grUqj&oRPoS-q5C?LfQG1iMv~ynHdObrs=DF-f`QKd@ap}PC zWAbp5u3H5_^4ox=%z4YG*ly`%C0K(x#2N_Mvu0LSE+>%?W6OG(BwLe&K-}9rOrT6- z{j8tuKs_7(E4e)Uqil4O*L(AqeiCSGV~1ei;I4yzDfhTn|3j);CmL!Fy3WzbHrZ8n z0zuMfRk1Dj^&D^4ylhL4StAl8%o9JqT*}9!1}54EJD-tq&5&JaVOz8R^c zyBx>&+OLN(A@Eg+%-4nPGK}qOL}r+Ci%L^+CDsN^(RowiDoAmuz=N?o_a1ma{_@nD z1s>BkHxBEdjo4gMWrENj%ssKR(|UP2|GtKm;S2oz2pF3YUK5-97q|TMdg~-!UH}Al z;D-ur?4V6*%LlU0#n3V!eX4vCn_8~vl7Xpp%A;g%xT0iIHNx+2HR#E~kJx)pu#RGX z-8_!vAzdG6G+|H}v!i+QHU`DTbWBUBWIK&b5pT`jU>0uN zeHbeNPWJDpb+v09u?Z`bR+#QDEt9y^q;o3sl(YP?Ju22ICAXcnxQh^HCPkAtas2(Z z=;E0-WQWuBn1U&tzy^Wd>!J8TH0M$35TunwCp8j{6JI~=zKt>J^O7e zUbr+B$mH!T%XQ&}j3^_G7=53RthhDSoQ^rr?c_ia*$%*N;uZhwx-!q_{N1tqD6T8h zi3T}oBV20a$V+yK14Zv8GAuskz;d>5{Xlk~`KElUd@N`dRX|Zv`LrbAOGadjor2-9 zcj!3tF~W;m{-7n@1O!aHeC!DOkfja)BfYS-P!>DRurEJuscE%NET05_CI-?pwb3rD z{@m1V3Fr5&SWu?S8%vVUgb!XhPccV?Z+PMT6y=_J!d{{Ij3Gs$I?^h8GQTlGdI3|t zN-%-@(742CK9hjXU%0w4gCOaGYC5F0UNrW1lDmxT?PuA*prRBhi~1P$xL!iAvdOAu zz|O~issPM7KYsihVTN_E@}BN|0Q?+H)G>h3gCfUZO)igQTK$NsC5qJwWZ2ggandd! zHcs5CSsde0S`m1P9)`k-zFev+a}qJcG%EL~w;G!8*yz2tZeNwnxQ?45z1b@Bq5h|K z*GKtHJ1YCe$|dTV@gtrp-s9W}OWC`4vNZ4~2QcOWJ{wx`wr)!*P(Qxhh~OmAC$yj7 z5RVy@0>m#4`p%0K&#;SE^Oi>1@mYpTjIbFdy07@B%3s_q#7IPk$rDF>y1lG*(jOx+ zFTfa8iAVwE*$M^W5hhg$#8b?l^fr&1ECKn=Y3s5rCA4)Tf7*S;5}RTB4nC)peWia zkwm~z!t-FJZLLvX6j1<-$fp|yDSwJJUu22l0oRTWnQ{p&NpL1t%67c zDd%>q0JmSst7paoCx@4V|F;UTEz!0Tu_Z#^DzwDoq|ZkWgQh*sMM5diIax>7^7B8r z>z-)+v)58^TqaQ%af{Otpd)zQ8=-poZbc#j%EygJfWUs4PYAXKN{anMVm8K3ZG-fB zS#?X!9PYiu9|8JrIfLC`-31b5-Na$Vf80=X?O;-JPcx(g4@=u{w0W$P7}R?ovB0cy zR7d6lroloG)So<6r9uu{lKfAm9*9YTfF5EN;N@y*2No+6R2{!$I}&XEIVeGH_=B0< zS1r;5CM%t{YL_w!#Vunc4#&ABGEWqJ>{!u;8uPdQ@k3u(->WAA74VRrMI$9yRigCc z{Te>A#vUdKGcN0m2>ZEu2RH9tq7VP37sLPO;L+?Y{3R0!RfZEuhxt01d%6@S(Orph zE{25)N=ji<79JWbqN^?a__?Yov$wbnJ0VNDR?_*&?OcP}LOe_En^vGDWy!@SLtVrN zsnNvl)Pb`~RIuaw;r=wG?q0<1h!FJeA%H+LguBs!&1mZelo}3q|YyN5$hE$O$neD`3c)c{+#+Y40-h0 z?onl=A)DW`GfC?2d%W#Q`G!GpC8xb)&L=QRvU`~^8XT}vf(G6#?S_u={ zJ8bv?;rUj&^>|Ys_VvVO5&A9SN}aC`TP=q`ZaEyflOsrCWBh|2*lX>z=>BedsXk#y ztkZ7fZEU6*Zm0IqKAvC^N8%BmeB~D^KMOQF6x>avkO(`w|Gbar<3J=LAxk4>A|IHp zZT?l_5!;?Rn^R~4-f7sjIwwcUH&D~p@9XC%F8g}Q$K1Ub$A?~FHXz?=cy-#!-S6gu zZWJ%A!N5|1{Aqb#EWnVfb6G*uZupvJ=p;GAt=0X1-j{DdG^{o1TP&fgfDTt84J$?Z z(cB#iEE}?!+4p4%HMMGeh)8uJa3_@}WPL%s;m=>M0PAmzW@WdnI4AFl0~XT>dicLZ z-o_RJz?&A@4qGQdjUWu--L*A{;PL*WKgq+*KiBP1V#iJ~W~K`E#wh_zmVasDuV$1u zccmRO=2?zhZ&LLWT0%gVnrILpd$fP2|2AxDM>R0A5NkN;z(&LNxqB4V5|{r2=H6&l z+YFXY=@0JKiN{uDvj1Hq9P~mW0^J-Z(I6!ROa%dnD7eQ}jdj(PawQy#&~PY#+{(y> zuunX}B2--KReeHFWS(*N5x(_JnEU&%O;PDx6)#7xK5awpGg0<@E{jF;B%U|D?A83w zT5}!SI{wd7K>mMg!Y#RohKzB$fibU47=~t;md`x-qq@XiH{Sx4nDeve0w;@8#QUgH z3V`}NK@brZ zm!5?__7`(BHu^$Ro@dH5*mB_CrA+}xA53E1{Nu=~yN5pIVKL=84qs2X5<*d0XhwI3 z1RY-N_|uen%)1UWKh-)mk_|lMTH;MTS4%>g+#Bd=*Yk4DkH{O^y_LX2hgfC==(yW* z=~vP}4lpueDaNZf_-9aF*FG3w&~6B3Cx z_R4`fx~!}cz6{v(Zc01eY=5pfwB?&}8e~G)fiuMDKt;fvy(gGhoEBgJqPH`^I6Jb< zd4KZGZq(AeBh5f|1YyT}^f{iJ8PwShL-=02t(CL~3d&qt%)2(53`qN0h@tZ5{~ zh#&Kk7SY!pu1Ts#&-A(jj z4Lo1YFepzQ3+M1-S`e-?PdP?>X5odGOB5>?A$GOs_ksDh$1Y{opE02gi*M~8p+S(s z7*)B0QVH25Ox~K%9n9Kiw_Mpw4^wY$jBK~EMt>+Ub7E9AM7Mb!H~sC2$JB(wrV+%^ zSW`4Sl$_(X><^{=OT|Q1iIz8;-`#-}>U%R4Cx`lBB60eO-S|g^ z_G%So%%G3^iUk$1Wdn|zk}t0bfPwRgujYrfx&G7n>%-Z+JVTduEWY!WS1QEjGaKH^ z6(u&-gcGK=$YGZZ-SC9>kkw)|$fuA0op|5}0c4g+xy-b|hxf7}G3b^lC>&^regn{H z`ITu}F{;|W@F%nonla)jGvW+z@yF={F%1+)-b04zVim z#Zhv6r(NI)LK-df8wR}mg`BBo*K#zfe>PB_o)v2UhuG-+z_ z))C&=4y8{FuGgGlPV5%rNMMJq!nHYWkz?qN|nE=sw2fZY<){EC6BpR zN84fNH}mJ7jo?Ux}yU#7&e3#pzRCY&H16rP*_JX ze-v{3Q1Q%vJj#R#xp2i>)ZuLZBGop?o!Ij7iDK5;+z$_iu-ZB2w+k)wp;xMlaE6Qo zQ+?K9W(--C|9;8@V*7e%?T1o5^%ts0LSIVIb>B=D#ujh5GJsuaE@s&Ayn(3~6*~|Y z`zy&*H2^F;iP#L=%u#l}%$f^j={-`dYy0_YqICT?COxi&g#Ft^mNb0#Mu8>jcByl} z$be~)DP0B@2i!0ApkYH%b-N7dV82aQ3gILACwc|Dg)FdMr-4w83 zLca5PL1ez%z~V1I9f`gx`Jug4B3>{|av5`FphpV{Bw)isPJs5Ai=SmNn5p|t%`>*avwAd^SJGPD;^!m8&w ztA3*4N?-CQRfi!b$z{Jg`HJ5L9(54?w&0r#N6VyB?_8 z))Pt(FBV7csj)qDITMxVjUZYa7n~(Wk(I{8L}rL51rN?3;mb=gk^mf(bnRX-0%-EW zGx$~@f3Zo7BrS*Ub4{CdO5?xCobRjW9V;yx0z-;hd=d%Mw2GcTyd=glD-&-mOYp7@ zZ8z`fX^ZvTId^PeO}<;Z#g9lOEhVrR=Yu&a zyg6C8%KSu(AuYc(-VIGv_aeWrH)8Oig!Kyd`m75q9G{)P(%7EvF`wpzX%oykPY#|}E^CeTXO=jzRWgaD3Nbudr zPEK#$d*r%@UmmoSlsloKoR=Q$Ph6BdCJGA!3rNX{KE)2jvrE}wzcg&4^7~$DPLd@&ZKGKHost!Us1Q#c*FCab&YjJ( z{GoX7$jC#~5e0gD!ppG5$cwal@oT)BpWi+{xZIAqT#!e>$V%igKXW2KCSiyD_c0P+ zX$9LN_2Ep13l?)C2d`QR_-J}q7|yJAhlfNU+g8fqzJ&^mTD-zb*LZM|za?&UIA!*1 zdxs6hw;@{M`^l|*#dJGCxEwtv;iMW}_^^1KrQ;*BPOfXUZ>Ue#8UFX!z@qf!263U` z3fQ{W1|q+u`uc2K$%{blBYhSnjVJ})Ez{MbPWoo{Vf|BXv*K4bpG*>O389>joNU42 z(A&dO>daVaod|d#5mW+J_`dgv(c6wS^>M!c?f^mpzQJHd{pCLdP_Fb$7B)}0$38&2}7vo~4R?^t+mGgLbR z3I9Y9tKs#1_-OgR&-(#bh7;+kKne9qxTAF@6WD6heM|i{JrWH?C7gJa*hgiDeu||K zB2#I5Qt6+LtmgFQ<{M*5Q&PFU(A(|tsMGheAHMi(gycF&99had+G5~84inyQf-#zP zYQI%VfTf<0p@3`EQh|FQbqLTZRs9vmX|aF>*|GYQ^pSYFdy%h{sxmAnM8g4s54Z4_ z?RKNOb+LU07we5^I-5`tKQ!bEuVd86*{kW` zWtN8P{(KU#{^crD#MRr>usu;yM*cOo41I5ul7o(_FF*7=tPo0MLsEmQ$2J&< zIkQ|8d!VGDS6u5n6bP+beSIX*fKCW4dwHU)GtsW#ki-Owf=E@cY*TCOeJ=X{Tu0Q* z#F5d=LHZF2%bFZ2R!{DWM1GW17(d37c<4en~krMU|`k18rBMl2E7UkKiVjY~vZS!60EkPp2y z^FP5iv|Bod;+=ioJNJ6hsSEuG5y*kKzaQ|4D;g+56a*33bN zR=tHRQE^Aa2fq64(U!L9@u~3!{>bq2P{gq%OYe-fF(NLvJ}r)7fC<3}R3pkvxp)w% z_QjOJosUS7M=7*(j$4Zg>3Fa3;cKqvxRJhOI2leC`xm}Rwv{)1`O~|Qd;=q#QIm14 zYes@)@eoOA^&_S#+xoOa*c>=)IrZ}MS)UF3cd@*eXjWKSg5Aa^&H~7&f^5l&(U=h3 z*GfZ}`uBrgCN=S#Y!|p8|BtD+aENOCzK3BLaFB3@Zt0Gp5m358P`ZbfZfO|0yCtLq zl#ouPK}sYf1u0Pw0Y#M7_nCX|=lgsAfirW?dCuNzuf6wLF37AfwXAI}uOXi_Zmo($dTpF^} zH#*aULi-^xgbCD4x{DfZQ&7 zC8w6Zo6c^&H}}$dY$)SBEXtI$ckuN_Y0EsAX=$RB%d*Um7;4Ezmx|8zgwJXugji&E|C<7n7#T_IN3|j2 zAx|K(o;uY}Ucijjs9zm(>>;p`%~-E#G*4j`f@1+G)YT5uwYi(cWPFrSIyCrZgzCS> zvfZ;bNr&t2wd$ySh?1pXj`^}uyK&~KXp`ey5cBNy`b=PW^hpLM_kSoQ94N`->q1}P z2iP;A^7P%gv2l1SuS7ai@ZUaNw`%T6(|8z~_c@Ie=eNab<|H$3tZ9{fe>X+QdfDZ3 z>XvU6ME4!FH{Umny!7<#PUi}CXSA%;Dp+uW#IWDL`298HfAd#ede!VPeu#f;Qx zkUPeb<1A__SP@IATW*44v;Vv<21r_$=g?siwQz)Svpp`T+8;+d?~60-1racKDRCcl z&V%C&-kz%hIDh?o#6M7<57gS7aIh8DlF^Pe$GPwSgwTuUR`4(AhwRiP;wks>ay1z} zMRHCHE#N5J^T7LE^!buC!yhG)Ef)_{jHhBjan5cf3NUCs5gFTKQ2 z{A)@D@jtLc77t<$WJal9aQNPym@5;ix|b^$frW`^OCda^G^7&AzxXKx zjK-cRJ=$^a(GZg$#hx2iDQV39Tp(mM05z0pM*rY(RwVo&7WewgOjlXvC^=U`!heCT zL=|KpGITvG??=hScxqPBkFyisp{?Liw&wz8F#(8ouik3zkLRL`W^yAn2Lck&o$@EqXsfnX*YO@M|A&vgf;U|BS?vV}mxI-QA<%Jq!*|C%7QAx${O)Xp zFyKRv(!CrwQVcmk()*^@pf_4pAA#0uj0a}hU|@DWy-#h&6$fGCKV&ui*&Rj4Aj$qO z+8JhHBw<%cY9>LtvNF3Mx$HRJ+_=xLb|>P_tJ*+MZEK`m4oyJTG_TjF^Vj#!4+T<# z_w#P7I}aK_HR(-t>{1la?lFi&II_~cAsTu=qmoq6ku|B-JG1yE{l8^Lcp3PhGfA76 zQM2J9&vaNdCGX#TDw@k`Ja9s2o=;hF;zvvN;2xeb99Nm*(*ygDawr1>{Es^cVUN);bxn*D2d z;aLX9;-&9Ojde>tDvr#m+I6N*T0Z3@m{Q7BIVT7YMQq$Xh($ME+Ob)8X^9wz>aVdQ zkXeSTn);?%J0ut|B61s?=8zUo0|>iDZR|Za%We%99ZLG|^YzU7d<~tff{eFrG? zqxKx+tWUZ|QRJqk;A8)b;&w4*?p7$E``Hd(Y2jhOdzw|oz+Xy6mRL^`5X=ECH&W#VO zjYpcZ=iIloo1z+zEV3u+oBsVlS#Ja!e@@}U#klsY9W!WB;n>HK6*qQ#>fy+(SY_gq zQeYwILg2l!Nkal8Wi%)fGUkL=s|-`A3mbQHShIxLD)lYOo!$G+drQ7=DFiSQuVn1i z>=*Sz3KaWv-1b=Y-S1{=W8lQJ3pgXTE|!+(YF&!ZNq<|H4dFjYDW4Lsn)8eXWdozf zn<}U=U}w0An1ubMU2Q@5N9WdteiodU(s*?*cWN!=Q4+9{$nT+ZRiD-RjP}59rP2K6 zU-s4iSh;umf>^I}M^Ee@T=5g!Q)Xjw$lv`A4aL5p(&LqJ)rbCDKgNDlcd|xDda5IAX$A7!TEAXn5H3FH`7i_H*3DJC^!d>woEVen>HAKjZows6YUloT~ z(~(UPYEI_=3k_CGAo$?S5-Mj%dh=6aXA&}xms@2Osylee^{M7PnDidg&DX=jre!4G zEB+*4{8n7xpxcld_3)w z%VUwu~~DNc>YCOe)g=(Kz%HFUe|OY1)+IY8$qDi9defLE3#1bLTEjE&P(tSaaSzE3U)VxxMr& zB;I%cBuIDIz7JC*YpwN}1}O_$6JZY3_j&LIc$a6;K3vf)_0|XWrqCoST@{cA(Y3^X z+GrgQs^;DQy*M1Agn45TE))>u$^*nS$z$p>tvqC@aC32*X6#QzERvWGq$*XO&TKG1 z3}Zs3#Q1YZLJP*QLhh2cyd6XXCw2AmiPeP0h-w;ao1M?Ja|f3Lr={HMu%N$nS8e3j zHmZ+!{!N5-AQ6r@hr3Pal*g64FX-&?dmdWJh~<{S#EddP+?ff6OmG{r>PG@+$*3g#qoc1}G=EQ@< z9LW}{`X&BfSzAH~zUHK|l&ut!ELLVzMq0c~u?Q*oQxg?$r9M@GPS%~9&h$dtQo|d3 z0;jBa!f<3+5>$(S`kdY@*O};GF)8*qWacXnh ziUUR(PXE7qV|NL6gAyjQ66Aek&@YJ3n4P0_>#T^00+k@Y&VnFm95^n#tk6g{`}Kp6 zgwuUxWBTi}IyfgScCm2*Rz&dg58fI9bK@?Gni+QLh%?$^&w#|J`2Y8wc%dd5 z1JlMICLKx6Fz;zEbd^jX9l6%X&5FyK)XB;v>&^sUWYc9~pT*OBA1z19s!4~?-C?Ua zUsyV=2UTbNe3Z*5dE?MnKssxB#IprE83yX)11CX z*dp?wC?;!4A3K~ATldtVOU>fByYC(eQ*<6O`iSI zQ#S^rl{dVnS)iJxpY3uOMX_oWQhl_n{XtRFU?(R6_?-@2e?x$>kR4T8#x*^y1 zk&A`Dpu#~fJT9GQmQlN^o*a|uVc}LR)7&I@JF($PPejxN)oUEmzkoWEf!QiSu}kBl zm)ci|j+FM#`^QB#8(HkH-VnM3GslDgq@N>VhY$>Y=eofLVXtHJt#^TbyX(k|ZL?(0 zW*vOpSN|562}eA#fWMnnD69Tobv>?JBFd^s0l`zUu&;@f$*6p!{)3usBHX(HUF)Sr zLp9EB*Vn!5I;|K&jg%Xp3ZVaToGEg~MI*oxduG4iZ7#Ah-fIctMYF;fgNpe5yWJaXMHOw)D-O z{HB+*5W*)-9UMQ1S|tS2=I`~JC{F_vc)S@3nE3zj6~*`~MOuvM@!&qZYQLmrx}hx68?;yaCjkmbW*s-^%VpZufQOX#nyS zFQ(}^HBx8Obky<*74|?IZzGjRu;s}Ce+{|Mx(GCQEz+-s4@7VIq5$!^9uDQ9Pe1t7 zo}A5`_M0f0{u{!pX&}jLR>5qX_$O}a^0U5@#VN8G@>!wTQxq|1zqc~Zzqi!d-8P%GyBhI^(AGf%f=`p zx7If)0wx!n1le4<0AS`0dRbe2SMQ@2%)~pWRkk)#FWKDa z$P58^qyOCuUPc1QWR=!L4-dAG{$pK9zl!`m-eJ#~EQx%cr9P#2A-^P;+>$#DwBqO*8K2t={c0xX?8t*%lZ ze>~L*O_oQWQZ|*-ZVGyC4Q+t+7=k|+i@lG(K5Zw!!{ICVacEF{pXo#aMwW!eEw*!J zWr_?#AZ@}0qTS<@4r!gjX zzB~fHOFcmqb@p#btPJjJ5C$)fq#=#%Ek&h7zyB;F{F;Du+hxgHm1@Z=9zGg}RdJ(2 zo#eTs)j$;L$1vrsUv10%Cwp@Q@!?iDX5#~XX3WrW<-S^!zgTk8=J|V8?cfQ3dQa## zU+|JT@}JhcB#vmF`U-FM^n35eOiT`t89t0^IU2^Nr;ZE?b|#$3E4Gyp$8|^x zDXC)(s3uvu_3ycrk~}Hy#Yxz7>6WlMM@_1){Zf|4(2L~d5WE7<&wt^97=Z&wTZb)* zs1Gf2OOWywo_oMfzDj47N;hJ|yzhbllJSM%ScxCG_YQi5Tt|G|V>RzT>C_Y2pA=q8 z-s({*)RC%akDR(0?g^6}D4~?^%Tv`BVSUjq8bQd+{w>VxgZ|4b?gx}mZlwX)p6Pa> zF$DQkzDKbST=41nBn;c8@@vk>dII{#4As<8%X0l@maHFjEEYX?Qm0wQh+H|&8UjfY z#fLLpjf5*vDJ}7=ah~b>olh?xDM-8-FQGT~^;l{gqJlk%jrF^l_M9t*^b*OR!?Wac;izv}9XN-_`E@S;V;HSmh64udg&CH7W0M=I@nWmNtiK(gjK`s6)S%6X8cH zB`LyA;?M{|V`~dD|IO&ByZGX4c>2nfCte!f3f4Nq?f)DJtc)Koru8Txc$>>n=oUW( zsO%|)5Oa)c#`m`kr(v2j?m36_;_K3%vkrDmtuV+hP!@gfanHFjKXNgWf!}bJi3PPV zVrAAN7&V!eSo+~ts_Y)CCY9-11eeQYx8N_|O@72k2}tqUv7 z$Sq)NjMUh)qa#K_tdw)UlzFHOmLshjfZ}# zTM2%CO(fWJ;qo$3ea+kRO-aCt3b!{u$MnF+9D6l?}-)8N&-Cb3jd1RFaCrkOA_%+cxY0{01 zPl>gtU`%`SBn~Swki|OimEM?!Y+qa+iCdj1aSHfjlL!?;3v8Jiyw+#UDQhxS>w`*+ zTF+_R)O^|n!QnRrx{v-!WO8C5~6EKjTiA=O(fB zM6u5z9sp>3SYIIvKrsDsf4$}U>|3tG)9Ug*tz$txf6CN;oP+zvGt_(Pt$%DU6KYf! z1Vi~g4phVFS7ov+{VDdXS z?-d6uWHxfzkXNuys&Q6*?~CvMW$k{<6HvTZTdj#J(Cvt@g^#?=%!)$WH)ufaO}H{y zFz^dO}--;AF=w^}iU|{ea;$ zVGk`6X_D`^W_M;a1c)%Q%wWX#;id4Vix^asqCKMbYDb`dLZG)$2E0@k+Lv~-y!wIBc#?2tjrcF!m&Qriw0|E)~T;Z$90nllE-&=4TZ*Bf(v z7`MDv^RkBqBhuf`k}6Gtmr)GYw8vAb;^sRt`=5WuSUyH{!Pi56zL^aN6 z_%B3}kl-60+kLH(;e0i0aw*MWf3Ht(+zObOxW{lW?Azs`ni?+S!Q!C98JOzF$1K)v z-PLqJYMpc9-ItXjwz3N4BstZ>{Agl11{Sep15!%12DEJ-)N+ z694-wkrEJv_9*ICqsv|>>CS^Tsm>t{f!96_Uy6h9z+mS7nkH|;e4+Ogb&9xM&J!Z7 zXHH}gH3I=9ib|nux_9p@#q%s0FOCQ&am@l zI5#tvLOyHW3$902B$AT%upntjIFj%&lW7K&a@NfYJfm*bULf|pDqciiOXs(2M|pbp z&Ftq44SswJuoHqRji$I6dYt(bO3HEnTTb0nI~==K+*3}7fQq-hsbjDe{$i`fJlEvgbF0{WMFGmMD6Av2ah2ar@goFE&MkF7CF-d zhKM_^e}9N9aO)a-+gI#8b?1<-xgHqXtgjtm1kG?aOs(VDB2K|3bUzS!doZD_Um7jD z4kk+XXeG^;=&`-EO@Pu138}!OEUyxk$X}p%NGP`EGe?ZAa14fiMf}(HEhyUoTGm=6EB{0uvHLBsCe-6orXZIEKdy# zoC?AOSt^skcrZ_s&Uh7v-dvk7rUtMOyKK z%hSQulD}q`!_$z3;p-=ozAwD*7=4x*PJ+Cfckwbb>rqW9I}m=?hBP>*OwZ&S(RbJGHpci z87MM)5aBG#U*$^3{g4L&059*ze;(s_>O&zc@alVn?&=*supx&BL`tM&-ozPL(BQ;A z{u+g)s3F2I8~hWupR(7e%A~qh6lB`Z8y@;(1cmqYB_8WYppR3B^?!h+Oz+6g?XPzD zZ0;A(&H({7iO&)Xf-uFTc--I7d3O$4=FEZ{KwN8oEcr7h-*;<9^=mX1l#l`m3&iR# zQR>rcAR>)h3DpQ3z@H_!9KOHU3Jp)o%6U&1F5cT|k89vb6z>0cV2Yx_?n=D6#=YJ8 z_3?ObX9Le4@`!Wnh?Upeia7DIpKP_@rOo!L9QXj-17n zk8*bqKOTnriv%0wnmBQNIB?Ov`|G{O&M)y~TFrH^Zp@!7GuqbI!<(GkItiSM$Yl?a zs+Yj~hJI4;4P8-iX6UpGJQ8o&Z#^Uuc&6}iJx?)&VQ=9gfC)ib$J>W^l(zcLEGKF*?z4wk3D=JtZqGwd>wDeV4kC9DE^m9&8+|NF|Nc@yBVxygYQ| z5vNm>CV}VS0elStLlab5lJIH zs1Z8=$gL}^ts>WH$ntQ%^7ALOB;k(vQJ|rG0^`~BQP|PcTX1E522Ha8O3UCG`pP-6%*PrIh~LJ( zdtmRB=p)c!z_q-X&5gi_m=lr2Dad-P%h?{ZECY2PuXLIj^b+(G>58yLac z`}3n?-7pqMag;vi1N%FP@Y*WN4j|~Tb$@>ejwmr_vVt4zW#A@7maY6Hfn~p!Z@#dc z^fg7;uxEAcM?d#nRAWNIAo$lyjK*JAm>3xcTwsip-if8%EDwm-*Yl&QUh>Kp2iOmh ze7+%bCTIP7{cJ>h^$tXl9lsHhHkQs-q;tkMaPJE~Z0}vu{ipZt8d*h?n|Z8bgctiU zMlTt^c)^ChLf<-2mq!$Oj!@sVd`S=`s|`ab!SP=BxTz5uOiET(X=WM0$YQZRO@LD> ze29nHmy{a0sn!E;#$5*DB9gJL%By&GMWmazRx=s`qh6_bt3~+ai#i*8@4pp_^gr8- z#bNn&{i>m9^FwmO@kg-Pz5R6Ih>3aQB|2)DJ1irCCCwH$@>o?<_(Z67!J2F=%q+m} zdrVsGFLU_8>zCaYyX`c-LhXU1 zx1w+UAmBd&pPKj&hT5d?LjSG!c*i*0$XXyS=H~nt;{kF>E&aU@#|B1qA0t;^6b{j- z_{~y@^l*JLJFWe(OBpuAj8))vNN2SQ?PMD+tB535L@)8mfMt!5Y{Epvq4vUXI=L(C zCsOb~L7(#-%t{tSATcc%8>?(}QgQiVzd#?@9@lMw`}MZe?+$q1x+9$)~* z_wZZ6eb->=*Q>umEpxT~5G1SCij|w;-(Y!cNxVEwHUs6VQr3yQVKp*9ZICZU0?5OiV$7QiJhC1Bucv^eh{Sj@WHh@a* zDKE$qB@Q5cH_{%pDL!-#Rwk%c0S@EmS=D13R6=7*@T=BFMalLJA>0S~vJZ2p!>ic{ zZ?a5iUk-F96d>NbKK)IU_Iy*37E1RnT&r;2TwtileIN?I=mHB@l5?H0&FC26{{+1q zOv)6=26Ck)hQ-j4yHn~SwfH~$*ZYb3$eOw}DrMJ;4X#=Iq>v;YZ2-Lpx zey!ayU1}N?VlbmhqN$*TnPj=l4{)A%HbTTJj z+fUs+JmkwQLufZkA>V2Ae0Ca;=6@qYcUjO6ms?5~T`;mC#BbiO@5UqG?k!;l`C(f&x19;#R3M8Tk5n*l%HjFb=l{EX@m?7AHVYKR_13QB)^?j77 zT%T9$TN$6@dtt1T%}o^kP>t%QeI}J-^tZ<;vb{#t%utPB;B~4iRlI(E=*NFx*mF$Y zo_yI3T#7mdXQ(yTwK(6_E~ip&^K5vYCNK#M++zyG7y!}APSSbwVe>CgEC?7$H4Pvyk3~;_$n6>`l|B(? zkb*~bwSdu4bGJb3gNyKK`xtux6hvchYev*~D?nEreFKkQx2+-2{^4l3-JmMXd}e~Y z!v=kx=uT!q4TnROHT6dZg5|Wcs`LQ?lg6@|@o{(4%TpmB`X z5C}AL@bUPc4QRdr6_oI_rjtz0olxqNZd}-g!;AS0lj-S$7h5fkbA{aqH++@bAwC`; znMlrE%l!F~ip^g!^X#R2&-v#RsYIsXhYK*CEv7Ui%rPD(l8xw1UbxCV;E&eL_qXAd z^iM#Mp}u2G{k-$9L6)Tc9ai29E6S4dc>h2JcQVw$GxeC?`IoktAfcaGDkF+jR1uBp z-nDRC5Kx=ndh$IaaBxoh#t~6nS#q4l~(F1Oq#UA+7ev!k?1QnyyWc%H+E)$LZpmT&rywM|(gO-GbA z0xJq{UB;BlF`g|4#_Qq!kUR{tvcGim58(x{)g#-OvgO&L#vgQ%a%UdWF2y^@EgtVDJ$Y=eXItp7X_qFt%64u= zypZ9Aa$DhV8|L)PnPu^Jvcj&<{|cB{ZQZge8T?DUx-vtr*+r)RG0&EMX?r+>{B z9*{uyDH0n}yB}mI;g8=Fu7R;1y-vU-PDylD&LKEs#_txj?`;>XEhAtKKMpX7}l z%x&^M@VNujoP6q|Vbh4%=12FHZ;10L%IUkoAQWk*<jERm_GY`+3yNdaPLW8A(`Kesh!Se>@09W4gSuGORcL$P&dt(&uOFm za@7d_Akd32s+CFirxbd<#`<)Moj=p5-rg7(j-yOOY;!cP>(3LMe_m=%?zVlI83iQ2iy@{2yw8Ygu4a0T8_8jW3|Ix)DR=9d-k>seoe8u{tJT;QPqGhw<8uR2{+ZyCpJj(RSYNh1Kv-n!s0@BfY7ED&m81$QGspma9C@)aS>n4p#FhpNuZPL@A*-2s zaB=8WsYA}X2P&cSidfG5a!_YV(_%@!+pCFi)3p@$MgQT77>`fkxa%X-RPKwA{{EQ2v5cSeX$W?UgKPcbbn#*Dahj}m^WF(U=1Q^0tR9!t{s~({inTuucwmgp6&Zi zykO=WF!5?N{N$9yFpGPPn=U#DBfh}2k~U2D1+)OijzN7k^#kVk?Huc_@$P#Vn5$VN z>8ok-7`Blp$`Yx%s>WpH@s#cC=goWprAGPb@}MNHlr-r9zY1+PoV||NmmZy-OHW+C$ag}DybjtCA4$R#?pDk z(I2{KOKd_87bkU%ufvG*-m{8b zrN}M7m_zcF6EWSwo(l13jLzV3x#dm76UrU=GuATa2u>4bwQGEy*|NbSR zfKaYcp#*<`+SPW1f^~v=iFl&t^aRP$B*eHu zL()er5>cWX6DPNeRZB{Br|d8IMEzlKx&=$+oDl11#I_v>sAI{FbxU1GMX}YOTYBVK#t!q_v+s?=jQ@ zG%j}J63PUsIdAg(VyDp|HbdU+x5f~7L}ox!jlLwZCl#oT$yZsODbxABf52bKD9KxY z`1Er1%j!92=|sQ9$-tD>NqG-!bfB!u)gjRqRm9&^+jWrDXv=IR1hK>RHg0OU9S)2s z$E>@CcEas$Mhq~ti->+{7-}p@{-!d#nuc*e7OV>9DG&(WBuMdRLT_dpT2LbutUqP& zg}cfz=Rb?KW&Wt8#~7HW#$+wi&&@`pf=<0xB&>Ob)f`KtqBc(oX@tSEa*uwp7r1O0 z$P6UlQev&yCb=~+ zvb8uc+Bo1w>0&I@eN!H#$AV(Cn5?U~X{Ss5H5N-T3D@&5H;y{SV$|ywj6h&$&&oLM z&D+t{y_xf~L&ah1kKcPpGsknQEk^I~{%)}@NPUPCTK+-pMKW3ooxoZ1SjvvKNHvd8 zR!)A2*XF%E+u#m=yzjy{xHda?8a;)`;#0X+pUEf8$~jA(VG}O63EbArm2MnY4irMX zRbSuSZur^uE4L3Lv;0lN$5Ihc?Z{|;K-Vq z7x>}Q2(&ahDtvXL0OPdLMI|s3B)b3##)+cAD|`6JX?H-EHkM_y96bjdr3}XMW;JL2l&Wy+k5foy*cvj-<~b^aw!^sO+$u-z)QJHi ztpSE82d4?-3eV6(k#d^1e5CGPHJ?Sj7pEEe(A3ZTWC=M6M6tfK{eHJdU@Q$Whf}iG z{_@CN0Tx%h&)a;J`tVM(EGa1FU6d%^Aj^1zbJ>2WfO+y5v7vFCg}=aZnckr^W78|w zc9`7IM>s?6Q2m}7=>gVQ-_#`!gVK?!qXa{1jvx>1I;&{l-OE2DOG72bfZN3}Fe@7z z9xBR~&@_6+LK8GH<@iAdx?7kWSE}$`>M+K@c%B3dv!R2WL|{@AhCj{iHikBC9EZ!@ zGDuU);WfBd&r(md?r49Z)zD(~I|!K0Jsx%SR71a72b4?UGs083C6fI_@;?S9j?X-W zxA|w_e;(ZCle-HoF%3@SMA+C`imS$yQkqaV*eW!*3`W!&Hln)$>i7HlO!>FsFG)** zs6xAAA`KB$+R@E9ZJ9OWlV|qzne)ovd06y5IDoV_`0TF6!SWDvG~X>*HE$)y4MTr+O2h$BU0rcpg3)bJ-R53MfmKUPOoBUit3`-(v=P`FQ4 zbc*T91jiW9&hUo+0m_t8JB9b9R;*IT^rYS@po#ldGi-_GUj5cjR#!sHF@1_n-+*tx z8kri58aEC+qeYk*7R#$q<~EVMQ*C8*WXtu5K5Ur;Q5o63SvTmq`qW_YcoOXbJ$ z$~yQc4)(`8L2c97nwH}ke(8awYfn#aC5k{#S!?%ZdvJD>*efDA<&2d0v?k3za(INi zPV0#25dRo*y5<&rzefeMG;=31F24becDuQV zZh%?66D)&tHT)6PFBFuFDYEgS0|WxAo{0%OiM}xpoT&?NH=l_3v4`QiKn|57FNI(Y zMxt-%ypwn!JPPOZVmBTfKEF})vZ550CrVfp(fE_7?!^l>z!0FrnJWLbao_B{6QjY& zUuWd7qONYxvz;O}6_WZkWFf_7IG#Ki7xkrsNF*M2POI$PS3}z}W`D>Xn5Md^*}PMD zC415QEG!)T0PCYswGWa5%4l`LX6?l-?E-(0 z(5m+33VO$DHKDn62?ma~vq*Qr?fq!+_28c!6aL~r^V`&K)tclYo-wAe^5?b%S8-CR zj#B6ctX}5f2ZGG7r6`ALoTwt}PZf{%F)Pvut!9PKCW*vXH#BD!fxYw4?CkSpUgh^D z_Q3MiYq1oS*BM}@Y@ggb&Ryd(n$zJ!Ck6QHRW8ft?Jbjq!Jk-eKd5p>i+E~qpj@Pj zBJ-QX8tY$V!mDAB;PQA59Jxk-(QjbfpWi%;VwNv{gM0KEJR~zB1CXchYhl2Z=Nm~5 z(oos@?Jg*y<>!>tjT0?T`P<(Ce8N3pV>gbcj-w>#I_V2@aD z{MDT>liF6vk=c!TETxpv67@iW=JGyar6ti~%td9S@cO;{7KS5jKXE3D>8Y)VC) zMWsH|m8MoGV>bm1YjbH5tL~*Mxbs!W#?Rkx+uEXk7&E|s3NqL7S{MZY}z$U2juNEsSiN0!aq^ZcmW$N-#y1d0}ka z<8dxnG9|1>rj~Q$o)O`>;p4Khd-tte9cnnv12>ss=`wsA9X4aDe~D0X7HcxQ%u8BD zUO8o}Kf}fzF%_GM|H$V^2&RE)X0ya0 z7rz$`#Z7IVZSxVs#}LpfOzrd{#gloHExNoAvC-UTN{teyncaH}VCP=$y^Lk8gO4Q$=i z%nQNT8ZU@^QrfUE8`Jtmi>TT+rZFr0Ml0XbLr9}N@=l{SHVR9!s6rAZo06YBTU}%8 z3;JAym~UGSQ9Jspwt8q>WhOhDli>%B&SZY$5Npx$>>~$9VTMYVD*cJsm8SU66(DHd z(fq+3E`F6S=)9BKWNmSqNOrk%Z1+~geUe+)%b@F~oa!V@21R|@>)7KRSa!jTJIu>r zoknB)r@a=~LZ35^HO8D{97J!YT)Q4tb=-Ds9H!uBCv0S9TtjTV&eJRqdwXr>IZS?# zmS{?Gj5OyzQR!!(<^m(owEu2Bs@HAI*2LRzqSDgjtlyN z_Dy2+0@Tgc8%Np$fD5Wy#cQqM@+*od#$8pKx~mA`MA z{;}*88=9ulJvur2L*P&}j<#9IXlr96YG8_x!0ztxC`*x}R1{&)v?s*M`{?y)7uu^L zlMsO|iMP(DMt=eCU#yFg^@HXeq-3(BNDPEH=VQm#p5pk)H%MQf%pFqkYt(vpeSKcl zwbR^toc&Uw{k=fQo<_>*Q@@V^Qp>_=EX2{bC)iT*!qGD~Rq2;b%FAFIG&ijJf13C) z@=$s-Zs3|iNIpJu5Y3I!{1gI^=NmLi`{^OpakpWHAwu|@csR(nLq zcAmpvD~fZ=bRfa41>dv0Y$)Bl-1Qa=86GoA?g)+Lz7`KLan*fhFjmLw{W&L7=!G<7 z(*Ap_21!&jpOljZN}yvSqR2)q`wsB)TIG@)dIT5KeW;oy)*T24*ly{v#jtNSx4qxU z02@NQMT?-ip8+wPK(wYN0+5Jv2q8}5N#$YVD@-Q>TP}%>jZyYSxDsJcjnQCKI0VCW z>(OGvoVU7=oEZ63Gpojw@JDX~XCVPzgw{Y__iahs5)y`^yRX6M%;;XmbFLJhzk5@4 zsBN0$J@NhP8Y)OyJX{dg6>A(V2YLU%->@+%#Y916-#XB0)%sSbNg{#@AU}fq(Cx9M zyGAo}TLz8tq!SJ{cGugt|1P!OAh?h0r+VFM{?Zi>niQ4(ObM?cCJ!32vSNQiD>Br* zZ=e8{fhXaz^(%ZvL^jEy3%g_!IejSvUq7gcTx?^i{N6T@pLd`4Nt*Mbwp2;F8M)#L z@^gha8F%R-24vq%tYg%rQ2;W>zr6&^yeD?_^|E32(>4WptY>5k#rJWTM`~?ie1g`L z!L-0f#72SIB&?S0ss9$fN}FsKRik_g@SksE8GIV}^|@!bj4N7Q(wlbJZj4oZ5*fz^ z6M+0E!+I5y)uyk+m=d?S>?-8>D9|cP=huh0(>03FIFktwC}J+*k>fBbRn*)O#wE6$ zo0il^^CP|gMtDDHPtt7!=I1ut+#QN1{W)I0a=s)Buw?5BJ_#NITR1`i=kEml^YG*q z=j7=B2}Uj^f>m za4-STMmjO9(ea$&5~>+vO~A2uAjahOoR$M;`k8we@>C3-9^^-fjzkx zYGf(uD9FdriNzjPc-ZYV3K`LvYvYHNSmy1U(Z=Pu9SkI7slHDLoc0&`v}Zb31V|<9 zfRXslp1(g*!|1$eChqY%P7?sJv3;x%w?QItp9?zvY{x{MD-fl<0}!MaJLE-`a*JsEXoi)i$+2>zi&sp0@uPu-N#K{Lcf6F#wf*Yvf! zo2t(@isjey=;~wn5^C7@YZe1&&BmtK_*3IaWKhxs7BpEPSc60B$@@l{>6@zeBrnXx zp<8iz(tIBKQ8$K0fWYqUl9%T~kEE%O2ZmRpjG{p5@^=vW1~{{jzJxrOB$u^d%^4AP zq{cJjS-3`;63b}p@NHFRNp{EbV+>zUpD^dk=W1i{33N0Q92?x5}zgXK2#Ple+U=X@Y{Qqi-&PYMYj@tKCE5YeDku= zr_GLH!QK#Pl8Z}yYm1zW!qg=EwqjNZwP^^9-Z#4t9*SR6l5}Y}e{Fj3_LL|1fmw0*ylYQs508 z%nk_99>hNl%M>C5!QDLN&mMg19Dj_SlitDfV#E$_FNh=?rO)AAHbulcfA&hrC%QM(#}{TTDex)SeWCpRt)#hun0iJFquU=*48v9^uh1Y0oxQQXDk1nV)*tz2 z_I>13)D;*$^=%JY?EbjZmZnkQ(xe6&F}I+3^92Tun;~5jV0n-4k|fvHAp~8RP%G*~L!!qeKWrPM#qxn$eGvgwfc(^V z^K+mB{&h8BdU*OSVs4(%I7!iJ%cXVRpt)Ak)W^tR?^96pa4Y`extkVTw3gYJIK@ZYU$Fp%9Lk3u@*V&Sc_Pe=*#yx4HUhEHe_5BlbZ!H#NzEJg&nyU*#;#Iu!7Zf48vGD%gUwWrM zzcs%ju7Ca8M5?gsm(3VlyTOp@&r@SLQWV`5uGg1wo|iuLI%4NJEzDmqjwA-Q1EO`7 z)L#=cdZL?F(=S|`g6n^BaNFT#-l`NGRbtg7qIu4xl(g>y@as15p4=N{mt3}7GodpK zP}N*=-62B8jaV|x-i3s3zj;pW!#;Es4{#_;ZrEyXd5Yu{v zwwYN?bbK%A)jOJ1^GIYD`Q9+2&)Ik$eQSKfFh!a7|55eUaZz^P7bpya3_U}GNOw1g zB15;*-3THek|LsXcc*lxq?Dv|Nhl#9AP5M8A_$WAJbvHb{oMQi#Pgi9&ptcWUW;55 zg1!8lRRYw`(GbYK<$vToJ%=m3ZzSE;|EbujSRu@pQxt_B|Ju*1A`aWPd zQcE+`-YN65L4~)m#z<94fxO8f8ly%@?Dr^!F6MK`rF5<{G@=hBh-Eu}81>-pFZt#w z5dox5yad1H7?+d!Y+b+Ya-T(pFpb*4bkUQD>%iI~&%~nYp~rPjIpF__Ms?!kXp5^` z`a0anqoflZOGc_50nU^jR&{49srLq3`0~9UTaRgUpGoRhsck=`^Sf}JL)9Ikf(iOY zKod^5sK*RIul;J7xFVEWjH;I#iCB%_phR3zh z%=fI!PA=H44;JLxs{8c zGJf&NHEVTCa8|O>HcP+e(eL4-MP(|&3*jC;Wu~h4)wjkR-o@YKoZ*7#0wSxY4~2Le zCuRBOX#}xdtRhKyY~z95`b7WRC*WmbM!i}$Q}OM_Gsz)MG0ZL$f-cIU;qSLBoUvyt zDF*;$*5#fQSesvUXTPOThk>fh(r899z`u@Y;K0KeCzy)|;5z+VO*k-~UM^~j4$)%{ zVKB2O^by4J*BOOV4@D|N94Bp*u74+cR(|K4M7IX?JKhGGK!E^7<~!Efc+1^}J*M9` zLM^wEiT&|7Hl9?1+W_tdtbJAFy(=bUk%o!u4uElGkH)^R!Ay=4;0y)abUjw{*Ow|P z>!tbZYQ`ZT)72OCa_3U}Sw9DGKVNws-pIiH6;xj*atw4RQ~g^60OZ^&y8}DJ^l@=Y zAb{AG&a5eF^&1b^ft13z>+;V8578}-R8df>dU{m!7a6D)Y-qg%vZ(5}A+j zF2>1Nk4p(I-?xFVW8oemqRQ8zu%byc-pC6mCg3rH5IE^`mTHs3@u=XKucQK$KRcOMB;5HGS@bpzEBq(M{M)&{Pbg z6-zb5{IZ+*j+(slbUi8v^n-5Cer*1df$Wki`u!(l;tE9T9$}z}_=Tm7LXi7C4?_0! z&7n*^eWPOGD7oZU8{nQ*H%(n+wf`<{bACzfUoxwAOadd6Lt{R_Ih$8;KOc%GhSXGk zAua$aeV<3`6;%4)tYOrj{Q3<5$PF*YQ%l5P>@~^2Hy9h|l0~|!^e!*2-!!J>ZJ#~T zr^sAw6%1%x`#=#b0YZl#A1zH90_!5`dfa9{bujtkC$=(1`0tQ+3=~n~&ZL((JyN*1 z{{gw}#n$&S{1;~|sE3WFgMQP}vK>fM>lQuVj>RLX=^0xxza$tlF z5=HJm#(1b(w(O*0vp$?>Y8dv_OZF>r5)*ihieP`(PyKRHqn)X~|KkGCO>JZFr$hVu z1$!yWkGU8VIer6~gqXytKbCCD*ITjvjQgXDD}UY^in}$KF1*;SCdE&0WGeLw=nt_X znMnHr)R=mJKJj-VEioT99*yZpwRnC-WCDdj$pJuQ%TVmnjox|Y$P#UuV74!G&u(&O1yBEQ#leU;kSX|n6_kWlA~nC`Gi2zb-C z6|H&;uThrPgEy9$tle&iv$jW&vP;fAwH~fb#{!!r^MumS5;JY4^xiRNni$_=3;FHB zoiWzo=3tpFpnBV6It}=18f=Pf=1QttN7cjs{!QEgZJM<76JUmW6H;IR>MQIGN_RY% znzYI;9oqzycd+bas&v>=2}!a!1N<`_Yg#2|MR<~)0~yd(R+=>P7mw3&jBZtVD4RIe z#fHV&dng1N&nbma!U*O4W$^8O<8?1~59prXCQX-Rb{5F0+#dAs@Z=K?yW!mt_tJR$2lO--Kv{5zD`UgwFK)#WlP;%qnO3D5#iXWSwA%K(*J zUf&x$$a>nDF)pEE;yP?0unwgA0-4QgE0{VNUa)iD7F-MCzBt=`Bh7a(kEIC93;V@R zO$S@^Ie>#%$oZcxZ??06^)_1RR;@9)030h0VEJ}1@OvWZ(D1PCls`Fnvr;2nAwTII z2prAWhj|Vk_?_u&rCosgOOld#`fQXzwm$vNM{G({WYycx4yk63)Glt?-374smics1 z0QXRs;J*fO7L#CY?@@{|81I}RW!mlXbCqcz^Og#M% zL@X1D*y|yN{>sylucvxVUdytL*QyJSI+%v9A4%4TtL?Rrr%nEihonahA0{X)9c)Wa zFse`5lDI37;Kt+DERlqKkYlHMQ)THE)HFdojv~5iQSF93=J#iOZLl}>S*4rzqPdvl zEwbmzh~yYHyl(|#iI8^KZdH1)ZKToLJVREa8G#wtTJP8(s7lQLA|14iuERF4%a|iD{>nX)6X*e&So;F z&Wjdf6)?y$6zG4QBQruJn6znJ447>_q?nD@RqKG7%x!Tuo-9TK7y3ib{%uk9RHMbh z_X@ww-G*7Qa>zU1tE7q;?m~fxZH&URX_Cb%uCtUkFIne*kme#fdxTb|TMNdumhMpq z*kO~_JY(ffU(c)2nWzsg*KsVbN7YpnT`$+mlNCn9(jC z625%WaP^+nv%EMJ%-#Tvs4!-C!-4oiWcv-<)z6M^#xplvW~}h;NOEsWa6F8OZUhP*-hyMdaXS`e@n0gqcrD5#iBGQ$@?%#T|5`cy*er#+*o)Lw%WlK zCw=uC!W7<4d^F12>`{sFPqMW%f|ZmV1SUk`Qt!q1Wn8yaYJFv>0ntNjMJWLQf+GTC zpxn(+kcoikRpRsbcb*VE#xKOroL^xA65k$ZKZVzZvz0^-gLxHuX`AN9;7Z@1r99i; zb*0WwBjM)l$K`fr`W;rIwiSlyoBgS7y{AtmbJUxSD6ZWk?UxFW;f(8_ z28+|oX{3IP*#eERVrT&B*%pJQq48q#A+?uQ8dl#}Pw=eul84H@XBqck12IJKcJop= zIB^8gXwwVIn^Z)vTZJGlNOH^-QJ*jJeMPc*QAsVKh0=mr|7Kt4&nCl2M#tf_Nskcv zV_FNK0=dMP6URcy+#YHetZZtUt|7Ki{j~)|)KwW=uq$gcYuw z&QBin>s;s3>}PK6pw{@B20_O3BZ)oxp1d}{HRd->wl84B!>;S2mzs9`%3cd+F_H=c zb4ABwHu-?ebuPRMFa*n5K1WO{nMiBU%CD%Ybf7E?_&AYoK23U7U+2dMx-`~%)klj>5D&g4aBYo+^yJh}kWuF3>L+z^FzJhYlUH0*$G8OBRUD-=CWfTjv zQ}me3*4w2ZmZrZJ+Mj>Y&uZdi1;�jqf%D2Y3j!f^gN0!Te8z-tP!KgC-s0%rjAz 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Ł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 Z$Gkp}czx^QON{suz*PVM diff --git a/docs/src/archive/images/op-restrict.png b/docs/src/archive/images/op-restrict.png deleted file mode 100644 index e686ac94aa62d639be9fd12c3896a30e099f8fc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45758 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 ea1wic{}ALRTb8is*O&p&OHN8jvR>Rc_-;&t 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 diff --git a/docs/src/archive/images/pipeline-database.png b/docs/src/archive/images/pipeline-database.png deleted file mode 100644 index 035df17cba7d745301e680cc9891ab557db14002..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104258 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 diff --git a/docs/src/archive/images/python_collection.png b/docs/src/archive/images/python_collection.png deleted file mode 100644 index 76fd1d7b010e1bc8fbaa0d182ba01d800bd2ab32..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61544 zcmce-WmKF^v?UB7xCaOpAcWxV4oPqi7Tn$4Ed&ehZoz}QO9PF&d*klbG}iRTd+(i@ z_s;y8H9x-U)vKRXT~&R$p7T`gI%n_j?}}2GXe4NGaB!G1(&8#`aLDiB;NUS(kX~z! z8da}eKah;&q{QK1|GfTmmL$H`pgKxxxxm4(Q2q0U_bd{1f2~Azl~IsD-bTa5W`9#0 z%j3|D=p)x|PdyA92!E9LFwCMd*U#Sr=zp#E>+1#k% za5k^g|3e3#f#H{O+(#VBcTGd@KI2F)eft>96c-XFPcwQEn;vIJLy9N83H24a_U!&_ zv7a^h;M;tY?b4;l{aSwix55R~qQt@dSBX!tG*%Y>chN2km-tr$TwJSon)ed_Zc!+a zp^5+RCh_lE8Q#vMIb36 zmhhq*p(U&TeT<5$to42`!C59L?9+%}Yr)Obdr&RizsCIZiFu~)e;*Go^1rp1`J})U z@$a9Mj^z5^2XR(M`Lh`tzw02NcR_%?mLnhq^J2Eubp5sF7EoY%;9XW1@fSX;3Lbb zUVPL}edApKFm&z>ANY-p$^n>t~*QJ0iFL|aj2EtVg;t1_Z+bLIB{=V3Q$b{UHBRz!p%A?M>$F8Thm#~vAA zu|Ku6lw+f}rjizN=9xwDkN^+xN$6IlY=Pela=NCv4LHFQYV(sAwh1U0Ti3~Yio2

fjF3z)ac3n+obe2bEQBNgYR)B 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�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%0D@fwFE)mmR&^Z%DL!-6VWN`Y&@<4i!wD<-D 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 diff --git a/docs/src/archive/images/restrict-example2.png b/docs/src/archive/images/restrict-example2.png deleted file mode 100644 index aa9a4636bdeec2106e1115057b283243a976a213..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24956 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�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 diff --git a/docs/src/archive/images/shapes_pipeline.svg b/docs/src/archive/images/shapes_pipeline.svg deleted file mode 100644 index 14572c4ce..000000000 --- a/docs/src/archive/images/shapes_pipeline.svg +++ /dev/null @@ -1,36 +0,0 @@ - - -%3 - - - -Area - - -Area - - - - - -Rectangle - - -Rectangle - - - - - -Rectangle->Area - - - - diff --git a/docs/src/archive/images/spawned-classes-ERD.svg b/docs/src/archive/images/spawned-classes-ERD.svg deleted file mode 100644 index 313841e81..000000000 --- a/docs/src/archive/images/spawned-classes-ERD.svg +++ /dev/null @@ -1,147 +0,0 @@ - - - -%3 - - - -Course - - -Course - - - - - -Section - - -Section - - - - - -Course->Section - - - - -Department - - -Department - - - - - -Department->Course - - - - -StudentMajor - - -StudentMajor - - - - - -Department->StudentMajor - - - - -Term - - -Term - - - - - -Term->Section - - - - -CurrentTerm - - -CurrentTerm - - - - - -Term->CurrentTerm - - - - -LetterGrade - - -LetterGrade - - - - - -Grade - - -Grade - - - - - -LetterGrade->Grade - - - - -Enroll - - -Enroll - - - - - -Enroll->Grade - - - - -Student - - -Student - - - - - -Student->Enroll - - - - -Student->StudentMajor - - - - -Section->Enroll - - - - diff --git a/docs/src/archive/images/union-example1.png b/docs/src/archive/images/union-example1.png deleted file mode 100644 index e693e7170445c2f491198bbf51393e3bf011c95e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11142 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 diff --git a/docs/src/archive/images/virtual-module-ERD.svg b/docs/src/archive/images/virtual-module-ERD.svg deleted file mode 100644 index 28eb0c481..000000000 --- a/docs/src/archive/images/virtual-module-ERD.svg +++ /dev/null @@ -1,147 +0,0 @@ - - - -%3 - - - -uni.LetterGrade - - -uni.LetterGrade - - - - - -uni.Grade - - -uni.Grade - - - - - -uni.LetterGrade->uni.Grade - - - - -uni.Course - - -uni.Course - - - - - -uni.Section - - -uni.Section - - - - - -uni.Course->uni.Section - - - - -uni.Term - - -uni.Term - - - - - -uni.Term->uni.Section - - - - -uni.CurrentTerm - - -uni.CurrentTerm - - - - - -uni.Term->uni.CurrentTerm - - - - -uni.Enroll - - -uni.Enroll - - - - - -uni.Section->uni.Enroll - - - - -uni.StudentMajor - - -uni.StudentMajor - - - - - -uni.Enroll->uni.Grade - - - - -uni.Department - - -uni.Department - - - - - -uni.Department->uni.Course - - - - -uni.Department->uni.StudentMajor - - - - -uni.Student - - -uni.Student - - - - - -uni.Student->uni.StudentMajor - - - - -uni.Student->uni.Enroll - - - - diff --git a/docs/src/archive/manipulation/delete.md b/docs/src/archive/manipulation/delete.md deleted file mode 100644 index e61e8a2b8..000000000 --- a/docs/src/archive/manipulation/delete.md +++ /dev/null @@ -1,31 +0,0 @@ -# Delete - -The `delete` method deletes entities from a table and all dependent entries in -dependent tables. - -Delete is often used in conjunction with the [restriction](../query/restrict.md) -operator to define the subset of entities to delete. -Delete is performed as an atomic transaction so that partial deletes never occur. - -## Examples - -```python -# delete all entries from tuning.VonMises -tuning.VonMises.delete() - -# delete entries from tuning.VonMises for mouse 1010 -(tuning.VonMises & 'mouse=1010').delete() - -# delete entries from tuning.VonMises except mouse 1010 -(tuning.VonMises - 'mouse=1010').delete() -``` - -## Deleting from part tables - -Entities in a [part table](../design/tables/master-part.md) are usually removed as a -consequence of deleting the master table. - -To enforce this workflow, calling `delete` directly on a part table produces an error. -In some cases, it may be necessary to override this behavior using the `part_integrity` parameter: -- `part_integrity="ignore"`: Remove entities from a part table without deleting from master (breaks integrity). -- `part_integrity="cascade"`: Delete from parts and also cascade up to delete the corresponding master entries. diff --git a/docs/src/archive/manipulation/index.md b/docs/src/archive/manipulation/index.md deleted file mode 100644 index 295195778..000000000 --- a/docs/src/archive/manipulation/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Data Manipulation - -Data **manipulation** operations change the state of the data stored in the database -without modifying the structure of the stored data. -These operations include [insert](insert.md), [delete](delete.md), and -[update](update.md). - -Data manipulation operations in DataJoint respect the -[integrity](../design/integrity.md) constraints. diff --git a/docs/src/archive/manipulation/insert.md b/docs/src/archive/manipulation/insert.md deleted file mode 100644 index 2db4157d6..000000000 --- a/docs/src/archive/manipulation/insert.md +++ /dev/null @@ -1,173 +0,0 @@ -# Insert - -The `insert` method of DataJoint table objects inserts entities into the table. - -In Python there is a separate method `insert1` to insert one entity at a time. -The entity may have the form of a Python dictionary with key names matching the -attribute names in the table. - -```python -lab.Person.insert1( - dict(username='alice', - first_name='Alice', - last_name='Cooper')) -``` - -The entity also may take the form of a sequence of values in the same order as the -attributes in the table. - -```python -lab.Person.insert1(['alice', 'Alice', 'Cooper']) -``` - -Additionally, the entity may be inserted as a -[NumPy record array](https://docs.scipy.org/doc/numpy/reference/generated/numpy.record.html#numpy.record) - or [Pandas DataFrame](https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html). - -The `insert` method accepts a sequence or a generator of multiple entities and is used -to insert multiple entities at once. - -```python -lab.Person.insert([ - ['alice', 'Alice', 'Cooper'], - ['bob', 'Bob', 'Dylan'], - ['carol', 'Carol', 'Douglas']]) -``` - -Several optional parameters can be used with `insert`: - - `replace` If `True`, replaces the existing entity. - (Default `False`.) - - `skip_duplicates` If `True`, silently skip duplicate inserts. - (Default `False`.) - - `ignore_extra_fields` If `False`, fields that are not in the heading raise an error. - (Default `False`.) - - `allow_direct_insert` If `True`, allows inserts outside of populate calls. - Applies only in auto-populated tables. - (Default `None`.) - -## Batched inserts - -Inserting a set of entities in a single `insert` differs from inserting the same set of -entities one-by-one in a `for` loop in two ways: - -1. Network overhead is reduced. - Network overhead can be tens of milliseconds per query. - Inserting 1000 entities in a single `insert` call may save a few seconds over - inserting them individually. -2. The insert is performed as an all-or-nothing transaction. - If even one insert fails because it violates any constraint, then none of the - entities in the set are inserted. - -However, inserting too many entities in a single query may run against buffer size or -packet size limits of the database server. -Due to these limitations, performing inserts of very large numbers of entities should -be broken up into moderately sized batches, such as a few hundred at a time. - -## Server-side inserts - -Data inserted into a table often come from other tables already present on the database server. -In such cases, data can be [fetched](../query/fetch.md) from the first table and then -inserted into another table, but this results in transfers back and forth between the -database and the local system. -Instead, data can be inserted from one table into another without transfers between the -database and the local system using [queries](../query/principles.md). - -In the example below, a new schema has been created in preparation for phase two of a -project. -Experimental protocols from the first phase of the project will be reused in the second -phase. -Since the entities are already present on the database in the `Protocol` table of the -`phase_one` schema, we can perform a server-side insert into `phase_two.Protocol` -without fetching a local copy. - -```python -# Server-side inserts are faster... -phase_two.Protocol.insert(phase_one.Protocol) - -# ...than fetching before inserting -protocols = phase_one.Protocol.fetch() -phase_two.Protocol.insert(protocols) -``` - -## Object attributes - -Tables with [`object`](../design/tables/object.md) type attributes can be inserted with -local file paths, folder paths, remote URLs, or streams. The content is automatically -copied to object storage. - -```python -# Insert with local file path -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "/local/path/to/data.dat" -}) - -# Insert with local folder path -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "/local/path/to/data_folder/" -}) - -# Insert from remote URL (S3, GCS, Azure, HTTP) -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": "s3://source-bucket/path/to/data.dat" -}) - -# Insert remote Zarr store (e.g., from collaborator) -Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "neural_data": "gs://collaborator-bucket/shared/experiment.zarr" -}) - -# Insert from stream with explicit extension -with open("/path/to/data.bin", "rb") as f: - Recording.insert1({ - "subject_id": 123, - "session_id": 45, - "raw_data": (".bin", f) - }) -``` - -Supported remote URL protocols: `s3://`, `gs://`, `az://`, `http://`, `https://` - -### Staged inserts - -For large objects like Zarr arrays, use `staged_insert1` to write directly to storage -without creating a local copy first: - -```python -import zarr - -with Recording.staged_insert1 as staged: - # Set primary key values first - staged.rec['subject_id'] = 123 - staged.rec['session_id'] = 45 - - # Create Zarr array directly in object storage - z = zarr.open(staged.store('raw_data', '.zarr'), mode='w', shape=(10000, 10000)) - z[:] = compute_large_array() - - # Assign to record - staged.rec['raw_data'] = z - -# On successful exit: metadata computed, record inserted -# On exception: storage cleaned up, no record inserted -``` - -The `staged_insert1` context manager provides: - -- `staged.rec`: Dict for setting attribute values -- `staged.store(field, ext)`: Returns fsspec store for Zarr/xarray -- `staged.open(field, ext, mode)`: Returns file handle for writing -- `staged.fs`: Direct fsspec filesystem access - -See the [object type documentation](../design/tables/object.md) for more details. diff --git a/docs/src/archive/manipulation/transactions.md b/docs/src/archive/manipulation/transactions.md deleted file mode 100644 index 58b9a3167..000000000 --- a/docs/src/archive/manipulation/transactions.md +++ /dev/null @@ -1,36 +0,0 @@ -# Transactions - -In some cases, a sequence of several operations must be performed as a single -operation: -interrupting the sequence of such operations halfway would leave the data in an invalid -state. -While the sequence is in progress, other processes accessing the database will not see -the partial results until the transaction is complete. -The sequence may include [data queries](../query/principles.md) and -[manipulations](index.md). - -In such cases, the sequence of operations may be enclosed in a transaction. - -Transactions are formed using the `transaction` property of the connection object. -The connection object may be obtained from any table object. -The `transaction` property can then be used as a context manager in Python's `with` -statement. - -For example, the following code inserts matching entries for the master table `Session` -and its part table `Session.Experimenter`. - -```python -# get the connection object -connection = Session.connection - -# insert Session and Session.Experimenter entries in a transaction -with connection.transaction: - key = {'subject_id': animal_id, 'session_time': session_time} - Session.insert1({**key, 'brain_region':region, 'cortical_layer':layer}) - Session.Experimenter.insert1({**key, 'experimenter': username}) -``` - -Here, to external observers, both inserts will take effect together upon exiting from -the `with` block or will not have any effect at all. -For example, if the second insert fails due to an error, the first insert will be -rolled back. diff --git a/docs/src/archive/manipulation/update.md b/docs/src/archive/manipulation/update.md deleted file mode 100644 index 7faa7cb87..000000000 --- a/docs/src/archive/manipulation/update.md +++ /dev/null @@ -1,48 +0,0 @@ -# Cautious Update - -In database programming, the **update** operation refers to modifying the values of -individual attributes in an entity within a table without replacing the entire entity. -Such an in-place update mechanism is not part of DataJoint's data manipulation model, -because it circumvents data -[dependency constraints](../design/integrity.md#referential-integrity). - -This is not to say that data cannot be changed once they are part of a pipeline. -In DataJoint, data is changed by replacing entire entities rather than by updating the -values of their attributes. -The process of deleting existing entities and inserting new entities with corrected -values ensures the [integrity](../design/integrity.md) of the data throughout the -pipeline. - -This approach applies specifically to automated tables -(see [Auto-populated tables](../compute/populate.md)). -However, manual tables are often edited outside DataJoint through other interfaces. -It is up to the user's discretion to allow updates in manual tables, and the user must -be cognizant of the fact that updates will not trigger re-computation of dependent data. - -## Usage - -For some cases, it becomes necessary to deliberately correct existing values where a -user has chosen to accept the above responsibility despite the caution. - -The `update1` method accomplishes this if the record already exists. Note that updates -to primary key values are not allowed. - -The method should only be used to fix problems, and not as part of a regular workflow. -When updating an entry, make sure that any information stored in dependent tables that -depends on the update values is properly updated as well. - -## Examples - -```python -# with record as a dict specifying the primary and -# secondary attribute values -table.update1(record) - -# update value in record with id as primary key -table.update1({'id': 1, 'value': 3}) - -# reset value to default with id as primary key -table.update1({'id': 1, 'value': None}) -# or -table.update1({'id': 1}) -``` diff --git a/docs/src/archive/publish-data.md b/docs/src/archive/publish-data.md deleted file mode 100644 index 3ec2d7211..000000000 --- a/docs/src/archive/publish-data.md +++ /dev/null @@ -1,34 +0,0 @@ -# Publishing Data - -DataJoint is a framework for building data pipelines that support rigorous flow of -structured data between experimenters, data scientists, and computing agents *during* -data acquisition and processing within a centralized project. -Publishing final datasets for the outside world may require additional steps and -conversion. - -## Provide access to a DataJoint server - -One approach for publishing data is to grant public access to an existing pipeline. -Then public users will be able to query the data pipelines using DataJoint's query -language and output interfaces just like any other users of the pipeline. -For security, this may require synchronizing the data onto a separate read-only public -server. - -## Containerizing as a DataJoint pipeline - -Containerization platforms such as [Docker](https://www.docker.com/) allow convenient -distribution of environments including database services and data. -It is convenient to publish DataJoint pipelines as a docker container that deploys the -populated DataJoint pipeline. -One example of publishing a DataJoint pipeline as a docker container is -> Sinz, F., Ecker, A.S., Fahey, P., Walker, E., Cobos, E., Froudarakis, E., Yatsenko, D., Pitkow, Z., Reimer, J. and Tolias, A., 2018. Stimulus domain transfer in recurrent models for large scale cortical population prediction on video. In Advances in Neural Information Processing Systems (pp. 7198-7209). https://www.biorxiv.org/content/early/2018/10/25/452672 - -The code and the data can be found at [https://github.com/sinzlab/Sinz2018_NIPS](https://github.com/sinzlab/Sinz2018_NIPS). - -## Exporting into a collection of files - -Another option for publishing and archiving data is to export the data from the -DataJoint pipeline into a collection of files. -DataJoint provides features for exporting and importing sections of the pipeline. -Several ongoing projects are implementing the capability to export from DataJoint -pipelines into [Neurodata Without Borders](https://www.nwb.org/) files. diff --git a/docs/src/archive/query/aggregation.md b/docs/src/archive/query/aggregation.md deleted file mode 100644 index e47fd0b33..000000000 --- a/docs/src/archive/query/aggregation.md +++ /dev/null @@ -1,29 +0,0 @@ -# Aggr - -**Aggregation**, performed with the `aggr` operator, is a special form of `proj` with -the additional feature of allowing aggregation calculations on another table. -It has the form `tab.aggr(other, ...)` where `other` is another table. -Without the argument `other`, `aggr` and `proj` are exactly equivalent. -Aggregation allows adding calculated attributes to each entity in `tab` based on -aggregation functions over attributes in the -[matching](./operators.md#matching-entities) entities of `other`. - -Aggregation functions include `count`, `sum`, `min`, `max`, `avg`, `std`, `variance`, -and others. -Aggregation functions can only be used in the definitions of new attributes within the -`aggr` operator. - -As with `proj`, the output of `aggr` has the same entity class, the same primary key, -and the same number of elements as `tab`. -Primary key attributes are always included in the output and may be renamed, just like -in `proj`. - -## Examples - -```python -# Number of students in each course section -Section.aggr(Enroll, n="count(*)") - -# Average grade in each course -Course.aggr(Grade * LetterGrade, avg_grade="avg(points)") -``` diff --git a/docs/src/archive/query/example-schema.md b/docs/src/archive/query/example-schema.md deleted file mode 100644 index 063e36574..000000000 --- a/docs/src/archive/query/example-schema.md +++ /dev/null @@ -1,112 +0,0 @@ -# Example Schema - -The example schema below contains data for a university enrollment system. -Information about students, departments, courses, etc. are organized in multiple tables. - -Warning: - Empty primary keys, such as in the `CurrentTerm` table, are not yet supported by - DataJoint. - This feature will become available in a future release. - See [Issue #113](https://github.com/datajoint/datajoint-python/issues/113) for more - information. - -```python -@schema -class Student (dj.Manual): -definition = """ -student_id : int unsigned # university ID ---- -first_name : varchar(40) -last_name : varchar(40) -sex : enum('F', 'M', 'U') -date_of_birth : date -home_address : varchar(200) # street address -home_city : varchar(30) -home_state : char(2) # two-letter abbreviation -home_zipcode : char(10) -home_phone : varchar(14) -""" - -@schema -class Department (dj.Manual): -definition = """ -dept : char(6) # abbreviated department name, e.g. BIOL ---- -dept_name : varchar(200) # full department name -dept_address : varchar(200) # mailing address -dept_phone : varchar(14) -""" - -@schema -class StudentMajor (dj.Manual): -definition = """ --> Student ---- --> Department -declare_date : date # when student declared her major -""" - -@schema -class Course (dj.Manual): -definition = """ --> Department -course : int unsigned # course number, e.g. 1010 ---- -course_name : varchar(200) # e.g. "Cell Biology" -credits : decimal(3,1) # number of credits earned by completing the course -""" - -@schema -class Term (dj.Manual): -definition = """ -term_year : year -term : enum('Spring', 'Summer', 'Fall') -""" - -@schema -class Section (dj.Manual): -definition = """ --> Course --> Term -section : char(1) ---- -room : varchar(12) # building and room code -""" - -@schema -class CurrentTerm (dj.Manual): -definition = """ ---- --> Term -""" - -@schema -class Enroll (dj.Manual): -definition = """ --> Section --> Student -""" - -@schema -class LetterGrade (dj.Manual): -definition = """ -grade : char(2) ---- -points : decimal(3,2) -""" - -@schema -class Grade (dj.Manual): -definition = """ --> Enroll ---- --> LetterGrade -""" -``` - -## Example schema diagram - -![University example schema](../images/queries_example_diagram.png){: style="align:center"} - -Example schema for a university database. -Tables contain data on students, departments, courses, etc. diff --git a/docs/src/archive/query/fetch.md b/docs/src/archive/query/fetch.md deleted file mode 100644 index 75a50fd0d..000000000 --- a/docs/src/archive/query/fetch.md +++ /dev/null @@ -1,174 +0,0 @@ -# Fetch - -Data queries in DataJoint comprise two distinct steps: - -1. Construct the `query` object to represent the required data using tables and -[operators](operators.md). -2. Fetch the data from `query` into the workspace of the host language -- described in -this section. - -Note that entities returned by `fetch` methods are not guaranteed to be sorted in any -particular order unless specifically requested. -Furthermore, the order is not guaranteed to be the same in any two queries, and the -contents of two identical queries may change between two sequential invocations unless -they are wrapped in a transaction. -Therefore, if you wish to fetch matching pairs of attributes, do so in one `fetch` call. - -The examples below are based on the [example schema](example-schema.md) for this part -of the documentation. - -## Entire table - -The following statement retrieves the entire table as a NumPy -[recarray](https://docs.scipy.org/doc/numpy/reference/generated/numpy.recarray.html). - -```python -data = query.fetch() -``` - -To retrieve the data as a list of `dict`: - -```python -data = query.fetch(as_dict=True) -``` - -In some cases, the amount of data returned by fetch can be quite large; in these cases -it can be useful to use the `size_on_disk` attribute to determine if running a bare -fetch would be wise. -Please note that it is only currently possible to query the size of entire tables -stored directly in the database at this time. - -## As separate variables - -```python -name, img = query.fetch1('name', 'image') # when query has exactly one entity -name, img = query.fetch('name', 'image') # [name, ...] [image, ...] -``` - -## Primary key values - -```python -keydict = tab.fetch1("KEY") # single key dict when tab has exactly one entity -keylist = tab.fetch("KEY") # list of key dictionaries [{}, ...] -``` - -`KEY` can also used when returning attribute values as separate variables, such that -one of the returned variables contains the entire primary keys. - -## Sorting and limiting the results - -To sort the result, use the `order_by` keyword argument. - -```python -# ascending order: -data = query.fetch(order_by='name') -# descending order: -data = query.fetch(order_by='name desc') -# by name first, year second: -data = query.fetch(order_by=('name desc', 'year')) -# sort by the primary key: -data = query.fetch(order_by='KEY') -# sort by name but for same names order by primary key: -data = query.fetch(order_by=('name', 'KEY desc')) -``` - -The `order_by` argument can be a string specifying the attribute to sort by. By default -the sort is in ascending order. Use `'attr desc'` to sort in descending order by -attribute `attr`. The value can also be a sequence of strings, in which case, the sort -performed on all the attributes jointly in the order specified. - -The special attribute name `'KEY'` represents the primary key attributes in order that -they appear in the index. Otherwise, this name can be used as any other argument. - -If an attribute happens to be a SQL reserved word, it needs to be enclosed in -backquotes. For example: - -```python -data = query.fetch(order_by='`select` desc') -``` - -The `order_by` value is eventually passed to the `ORDER BY` -[clause](https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html). - -Similarly, the `limit` and `offset` arguments can be used to limit the result to a -subset of entities. - -For example, one could do the following: - -```python -data = query.fetch(order_by='name', limit=10, offset=5) -``` - -Note that an `offset` cannot be used without specifying a `limit` as well. - -## Usage with Pandas - -The [pandas library](http://pandas.pydata.org/) is a popular library for data analysis -in Python which can easily be used with DataJoint query results. -Since the records returned by `fetch()` are contained within a `numpy.recarray`, they -can be easily converted to `pandas.DataFrame` objects by passing them into the -`pandas.DataFrame` constructor. -For example: - -```python -import pandas as pd -frame = pd.DataFrame(tab.fetch()) -``` - -Calling `fetch()` with the argument `format="frame"` returns results as -`pandas.DataFrame` objects indexed by the table's primary key attributes. - -```python -frame = tab.fetch(format="frame") -``` - -Returning results as a `DataFrame` is not possible when fetching a particular subset of -attributes or when `as_dict` is set to `True`. - -## Object Attributes - -When fetching [`object`](../design/tables/object.md) attributes, DataJoint returns an -`ObjectRef` handle instead of the raw data. This allows working with large files without -copying them locally. - -```python -record = Recording.fetch1() -obj = record["raw_data"] - -# Access metadata (no I/O) -print(obj.path) # Storage path -print(obj.size) # Size in bytes -print(obj.is_dir) # True if folder - -# Read content -content = obj.read() # Returns bytes for files - -# Open as file object -with obj.open() as f: - data = f.read() - -# Download to local path -local_path = obj.download("/local/destination/") -``` - -### Integration with Array Libraries - -`ObjectRef` provides direct fsspec access for Zarr and xarray: - -```python -import zarr -import xarray as xr - -obj = Recording.fetch1()["neural_data"] - -# Open as Zarr array -z = zarr.open(obj.store, mode='r') - -# Open with xarray -ds = xr.open_zarr(obj.store) - -# Direct filesystem access -fs = obj.fs -``` - -See the [object type documentation](../design/tables/object.md) for more details. diff --git a/docs/src/archive/query/iteration.md b/docs/src/archive/query/iteration.md deleted file mode 100644 index 60d95f107..000000000 --- a/docs/src/archive/query/iteration.md +++ /dev/null @@ -1,36 +0,0 @@ -# Iteration - -The DataJoint model primarily handles data as sets, in the form of tables. However, it -can sometimes be useful to access or to perform actions such as visualization upon -individual entities sequentially. In DataJoint this is accomplished through iteration. - -In the simple example below, iteration is used to display the names and values of the -attributes of each entity in the simple table or table expression. - -```python -for entity in table: - print(entity) -``` - -This example illustrates the function of the iterator: DataJoint iterates through the -whole table expression, returning the entire entity during each step. In this case, -each entity will be returned as a `dict` containing all attributes. - -At the start of the above loop, DataJoint internally fetches only the primary keys of -the entities. Since only the primary keys are needed to distinguish between entities, -DataJoint can then iterate over the list of primary keys to execute the loop. At each -step of the loop, DataJoint uses a single primary key to fetch an entire entity for use -in the iteration, such that `print(entity)` will print all attributes of each entity. -By first fetching only the primary keys and then fetching each entity individually, -DataJoint saves memory at the cost of network overhead. This can be particularly useful -for tables containing large amounts of data in secondary attributes. - -The memory savings of the above syntax may not be worth the additional network overhead -in all cases, such as for tables with little data stored as secondary attributes. In -the example below, DataJoint fetches all of the attributes of each entity in a single -call and then iterates over the list of entities stored in memory. - -```python -for entity in table.fetch(as_dict=True): - print(entity) -``` diff --git a/docs/src/archive/query/join.md b/docs/src/archive/query/join.md deleted file mode 100644 index d0ab0eae0..000000000 --- a/docs/src/archive/query/join.md +++ /dev/null @@ -1,37 +0,0 @@ -# Join - -## Join operator `*` - -The Join operator `A * B` combines the matching information in `A` and `B`. -The result contains all matching combinations of entities from both arguments. - -### Principles of joins - -1. The operands `A` and `B` must be **join-compatible**. -2. The primary key of the result is the union of the primary keys of the operands. - -### Examples of joins - -Example 1 : When the operands have no common attributes, the result is the cross -product -- all combinations of entities. - -![join-example1](../images/join-example1.png){: style="width:464px; align:center"} - -Example 2 : When the operands have common attributes, only entities with matching -values are kept. - -![join-example2](../images/join-example2.png){: style="width:689px; align:center"} - -Example 3 : Joining on secondary attribute. - -![join-example3](../images/join-example3.png){: style="width:689px; align:center"} - -### Properties of join - -1. When `A` and `B` have the same attributes, the join `A * B` becomes equivalent to -the set intersection `A` ∩ `B`. - Hence, DataJoint does not need a separate intersection operator. - -2. Commutativity: `A * B` is equivalent to `B * A`. - -3. Associativity: `(A * B) * C` is equivalent to `A * (B * C)`. diff --git a/docs/src/archive/query/operators.md b/docs/src/archive/query/operators.md deleted file mode 100644 index ee3549f35..000000000 --- a/docs/src/archive/query/operators.md +++ /dev/null @@ -1,395 +0,0 @@ -# Operators - -[Data queries](principles.md) have the form of expressions using operators to derive -the desired table. -The expressions themselves do not contain any data. -They represent the desired data symbolically. - -Once a query is formed, the [fetch](fetch.md) methods are used to bring the data into -the local workspace. -Since the expressions are only symbolic representations, repeated `fetch` calls may -yield different results as the state of the database is modified. - -DataJoint implements a complete algebra of operators on tables: - -| operator | notation | meaning | -|------------------------------|----------------|-------------------------------------------------------------------------| -| [join](#join) | A * B | All matching information from A and B | -| [restriction](#restriction) | A & cond | The subset of entities from A that meet the condition | -| [restriction](#restriction) | A - cond | The subset of entities from A that do not meet the condition | -| [proj](#proj) | A.proj(...) | Selects and renames attributes from A or computes new attributes | -| [aggr](#aggr) | A.aggr(B, ...) | Same as projection with computations based on matching information in B | -| [union](#union) | A + B | All unique entities from both A and B | -| [universal set](#universal-set)\*| dj.U() | All unique entities from both A and B | -| [top](#top)\*| dj.Top() | The top rows of A - -\*While not technically query operators, it is useful to discuss Universal Set and Top in the -same context. - -## Principles of relational algebra - -DataJoint's algebra improves upon the classical relational algebra and upon other query -languages to simplify and enhance the construction and interpretation of precise and -efficient data queries. - -1. **Entity integrity**: Data are represented and manipulated in the form of tables -representing [well-formed entity sets](../design/integrity.md). - This applies to the inputs and outputs of query operators. - The output of a query operator is an entity set with a well-defined entity type, a - primary key, unique attribute names, etc. -2. **Algebraic closure**: All operators operate on entity sets and yield entity sets. - Thus query expressions may be used as operands in other expressions or may be - assigned to variables to be used in other expressions. -3. **Attributes are identified by names**: All attributes have explicit names. - This includes results of queries. - Operators use attribute names to determine how to perform the operation. - The order of the attributes is not significant. - -## Matching entities - -Binary operators in DataJoint are based on the concept of **matching entities**; this -phrase will be used throughout the documentation. - - Two entities **match** when they have no common attributes or when their common - attributes contain the same values. - -Here **common attributes** are those that have the same names in both entities. -It is usually assumed that the common attributes are of compatible datatypes to allow -equality comparisons. - -Another way to phrase the same definition is - - Two entities match when they have no common attributes whose values differ. - -It may be conceptually convenient to imagine that all tables always have an additional -invisible attribute, `omega` whose domain comprises only one value, 1. -Then the definition of matching entities is simplified: - - Two entities match when their common attributes contain the same values. - -Matching entities can be **merged** into a single entity without any conflicts of -attribute names and values. - -### Examples - -This is a matching pair of entities: - -![matched_tuples1](../images/matched_tuples1.png){: style="width:366px"} - -and so is this one: - -![matched_tuples2](../images/matched_tuples2.png){: style="width:366px"} - -but these entities do *not* match: - -![matched_tuples3](../images/matched_tuples3.png){: style="width:366px"} - -## Join compatibility - -All binary operators with other tables as their two operands require that the operands -be **join-compatible**, which means that: - -1. All common attributes in both operands (attributes with the same name) must be part -of either the primary key or a foreign key. -2. All common attributes in the two relations must be of a compatible datatype for -equality comparisons. - -## Restriction - -The restriction operator `A & cond` selects the subset of entities from `A` that meet -the condition `cond`. The exclusion operator `A - cond` selects the complement of -restriction, i.e. the subset of entities from `A` that do not meet the condition -`cond`. This means that the restriction and exclusion operators are complementary. -The same query could be constructed using either `A & cond` or `A - Not(cond)`. - -

-![Restriction and exclusion.](../../../images/concepts-operators-restriction.png){: style="height:200px"} -
- -The condition `cond` may be one of the following: - -=== "Python" - - - another table - - a mapping, e.g. `dict` - - an expression in a character string - - a collection of conditions as a `list`, `tuple`, or Pandas `DataFrame` - - a Boolean expression (`True` or `False`) - - an `AndList` - - a `Not` object - - a query expression - -??? Warning "Permissive Operators" - - To circumvent compatibility checks, DataJoint offers permissive operators for - Restriction (`^`) and Join (`@`). Use with Caution. - -## Proj - -The `proj` operator represents **projection** and is used to select attributes -(columns) from a table, to rename them, or to create new calculated attributes. - -1. A simple projection *selects a subset of attributes* of the original -table, which may not include the [primary key](../concepts/glossary#primary-key). - -2. A more complex projection *renames an attribute* in another table. This could be -useful when one table should be referenced multiple times in another. A user table, -could contain all personnel. A project table references one person for the lead and -another the coordinator, both referencing the common personnel pool. - -3. Projection can also perform calculations (as available in -[MySQL](https://dev.mysql.com/doc/refman/5.7/en/functions.html)) on a single attribute. - -## Aggr - -**Aggregation** is a special form of `proj` with the added feature of allowing - aggregation calculations on another table. It has the form `table.aggr - (other, ...)` where `other` is another table. Aggregation allows adding calculated - attributes to each entity in `table` based on aggregation functions over attributes - in the matching entities of `other`. - -Aggregation functions include `count`, `sum`, `min`, `max`, `avg`, `std`, `variance`, -and others. - -## Union - -The result of the union operator `A + B` contains all the entities from both operands. - -[Entity normalization](../design/normalization) requires that `A` and `B` are of the same type, -with with the same [primary key](../concepts/glossary#primary-key), using homologous -attributes. Without secondary attributes, the result is the simple set union. With -secondary attributes, they must have the same names and datatypes. The two operands -must also be **disjoint**, without any duplicate primary key values across both inputs. -These requirements prevent ambiguity of attribute values and preserve entity identity. - -??? Note "Principles of union" - - 1. As in all operators, the order of the attributes in the operands is not - significant. - - 2. Operands `A` and `B` must have the same primary key attributes. Otherwise, an - error will be raised. - - 3. Operands `A` and `B` may not have any common non-key attributes. Otherwise, an - error will be raised. - - 4. The result `A + B` will have the same primary key as `A` and `B`. - - 5. The result `A + B` will have all the non-key attributes from both `A` and `B`. - - 6. For entities that are found in both `A` and `B` (based on the primary key), the - secondary attributes will be filled from the corresponding entities in `A` and - `B`. - - 7. For entities that are only found in either `A` or `B`, the other operand's - secondary attributes will filled with null values. - -For union, order does not matter. - -
-![Union Example 1](../../../images/concepts-operators-union1.png){: style="height:200px"} -
-
-![Union Example 2](../../../images/concepts-operators-union2.png){: style="height:200px"} -
- -??? Note "Properties of union" - - 1. Commutative: `A + B` is equivalent to `B + A`. - 2. Associative: `(A + B) + C` is equivalent to `A + (B + C)`. - -## Universal Set - -All of the above operators are designed to preserve their input type. Some queries may -require creating a new entity type not already represented by existing tables. This -means that the new type must be defined as part of the query. - -Universal sets fulfill this role using `dj.U` notation. They denote the set of all -possible entities with given attributes of any possible datatype. Attributes of -universal sets are allowed to be matched to any namesake attributes, even those that do -not come from the same initial source. - -Universal sets should be used sparingly when no suitable base tables already exist. In -some cases, defining a new base table can make queries clearer and more semantically -constrained. - -The examples below will use the table definitions in [table tiers](../reproduce/table-tiers). - - - -## Top - -Similar to the universal set operator, the top operator uses `dj.Top` notation. It is used to -restrict a query by the given `limit`, `order_by`, and `offset` parameters: - -```python -Session & dj.Top(limit=10, order_by='session_date') -``` - -The result of this expression returns the first 10 rows of `Session` and sorts them -by their `session_date` in ascending order. - -### `order_by` - -| Example | Description | -|-------------------------------------------|---------------------------------------------------------------------------------| -| `order_by="session_date DESC"` | Sort by `session_date` in *descending* order | -| `order_by="KEY"` | Sort by the primary key | -| `order_by="KEY DESC"` | Sort by the primary key in *descending* order | -| `order_by=["subject_id", "session_date"]` | Sort by `subject_id`, then sort matching `subject_id`s by their `session_date` | - -The default values for `dj.Top` parameters are `limit=1`, `order_by="KEY"`, and `offset=0`. - -## Restriction - -`&` and `-` operators permit restriction. - -### By a mapping - -For a [Session table](../reproduce/table-tiers#manual-tables), that has the attribute -`session_date`, we can restrict to sessions from January 1st, 2022: - -```python -Session & {'session_date': "2022-01-01"} -``` - -If there were any typos (e.g., using `sess_date` instead of `session_date`), our query -will return all of the entities of `Session`. - -### By a string - -Conditions may include arithmetic operations, functions, range tests, etc. Restriction -of table `A` by a string containing an attribute not found in table `A` produces an -error. - -```python -Session & 'user = "Alice"' # (1) -Session & 'session_date >= "2022-01-01"' # (2) -``` - -1. All the sessions performed by Alice -2. All of the sessions on or after January 1st, 2022 - -### By a collection - -When `cond` is a collection of conditions, the conditions are applied by logical -disjunction (logical OR). Restricting a table by a collection will return all entities -that meet *any* of the conditions in the collection. - -For example, if we restrict the `Session` table by a collection containing two -conditions, one for user and one for date, the query will return any sessions with a -matching user *or* date. - -A collection can be a list, a tuple, or a Pandas `DataFrame`. - -``` python -cond_list = ['user = "Alice"', 'session_date = "2022-01-01"'] # (1) -cond_tuple = ('user = "Alice"', 'session_date = "2022-01-01"') # (2) -import pandas as pd -cond_frame = pd.DataFrame(data={'user': ['Alice'], 'session_date': ['2022-01-01']}) # (3) - -Session() & ['user = "Alice"', 'session_date = "2022-01-01"'] -``` - -1. A list -2. A tuple -3. A data frame - -`dj.AndList` represents logical conjunction(logical AND). Restricting a table by an -`AndList` will return all entities that meet *all* of the conditions in the list. `A & -dj.AndList([c1, c2, c3])` is equivalent to `A & c1 & c2 & c3`. - -```python -Student() & dj.AndList(['user = "Alice"', 'session_date = "2022-01-01"']) -``` - -The above will show all the sessions that Alice conducted on the given day. - -### By a `Not` object - -The special function `dj.Not` represents logical negation, such that `A & dj.Not -(cond)` is equivalent to `A - cond`. - -### By a query - -Restriction by a query object is a generalization of restriction by a table. The example -below creates a query object corresponding to all the users named Alice. The `Session` -table is then restricted by the query object, returning all the sessions performed by -Alice. - -``` python -query = User & 'user = "Alice"' -Session & query -``` - -## Proj - -Renaming an attribute in python can be done via keyword arguments: - -```python -table.proj(new_attr='old_attr') -``` - -This can be done in the context of a table definition: - -```python -@schema -class Session(dj.Manual): - definition = """ - # Experiment Session - -> Animal - session : smallint # session number for the animal - --- - session_datetime : datetime # YYYY-MM-DD HH:MM:SS - session_start_time : float # seconds relative to session_datetime - session_end_time : float # seconds relative to session_datetime - -> User.proj(experimenter='username') - -> User.proj(supervisor='username') - """ -``` - -Or to rename multiple values in a table with the following syntax: -`Table.proj(*existing_attributes,*renamed_attributes)` - -```python -Session.proj('session','session_date',start='session_start_time',end='session_end_time') -``` - -Projection can also be used to to compute new attributes from existing ones. - -```python -Session.proj(duration='session_end_time-session_start_time') & 'duration > 10' -``` - -## Aggr - -For more complicated calculations, we can use aggregation. - -``` python -Subject.aggr(Session,n="count(*)") # (1) -Subject.aggr(Session,average_start="avg(session_start_time)") # (2) -``` - -1. Number of sessions per subject. -2. Average `session_start_time` for each subject - - - -## Universal set - -Universal sets offer the complete list of combinations of attributes. - -``` python -# All home cities of students -dj.U('laser_wavelength', 'laser_power') & Scan # (1) -dj.U('laser_wavelength', 'laser_power').aggr(Scan, n="count(*)") # (2) -dj.U().aggr(Session, n="max(session)") # (3) -``` - -1. All combinations of wavelength and power. -2. Total number of scans for each combination. -3. Largest session number. - -`dj.U()`, as shown in the last example above, is often useful for integer IDs. -For an example of this process, see the source code for -[Element Array Electrophysiology's `insert_new_params`](https://docs.datajoint.com/elements/element-array-ephys/latest/api/element_array_ephys/ephys_acute/#element_array_ephys.ephys_acute.ClusteringParamSet.insert_new_params). diff --git a/docs/src/archive/query/principles.md b/docs/src/archive/query/principles.md deleted file mode 100644 index 9b9fd284d..000000000 --- a/docs/src/archive/query/principles.md +++ /dev/null @@ -1,81 +0,0 @@ -# Query Principles - -**Data queries** retrieve data from the database. -A data query is performed with the help of a **query object**, which is a symbolic -representation of the query that does not in itself contain any actual data. -The simplest query object is an instance of a **table class**, representing the -contents of an entire table. - -For example, if `experiment.Session` is a DataJoint table class, you can create a query -object to retrieve its entire contents as follows: - -```python -query = experiment.Session() -``` - -More generally, a query object may be formed as a **query expression** constructed by -applying [operators](operators.md) to other query objects. - -For example, the following query retrieves information about all experiments and scans -for mouse 102 (excluding experiments with no scans): - -```python -query = experiment.Session * experiment.Scan & 'animal_id = 102' -``` - -Note that for brevity, query operators can be applied directly to class objects rather -than instance objects so that `experiment.Session` may be used in place of -`experiment.Session()`. - -You can preview the contents of the query in Python, Jupyter Notebook, or MATLAB by -simply displaying the object. -In the image below, the object `query` is first defined as a restriction of the table -`EEG` by values of the attribute `eeg_sample_rate` greater than 1000 Hz. -Displaying the object gives a preview of the entities that will be returned by `query`. -Note that this preview only lists a few of the entities that will be returned. -Also, the preview does not contain any data for attributes of datatype `blob`. - -![Query object preview](../images/query_object_preview.png){: style="align:center"} - -Defining a query object and previewing the entities returned by the query. - -Once the desired query object is formed, the query can be executed using its -[fetch](fetch.md) methods. -To **fetch** means to transfer the data represented by the query object from the -database server into the workspace of the host language. - -```python -s = query.fetch() -``` - -Here fetching from the `query` object produces the NumPy record array `s` of the -queried data. - -## Checking for returned entities - -The preview of the query object shown above displayed only a few of the entities -returned by the query but also displayed the total number of entities that would be -returned. -It can be useful to know the number of entities returned by a query, or even whether a -query will return any entities at all, without having to fetch all the data themselves. - -The `bool` function applied to a query object evaluates to `True` if the query returns -any entities and to `False` if the query result is empty. - -The `len` function applied to a query object determines the number of entities returned -by the query. - -```python -# number of sessions since the start of 2018. -n = len(Session & 'session_date >= "2018-01-01"') -``` - -## Normalization in queries - -Query objects adhere to entity [entity normalization](../design/normalization.md) just -like the stored tables do. -The result of a query is a well-defined entity set with an readily identifiable entity -class and designated primary attributes that jointly distinguish any two entities from -each other. -The query [operators](operators.md) are designed to keep the result normalized even in -complex query expressions. diff --git a/docs/src/archive/query/project.md b/docs/src/archive/query/project.md deleted file mode 100644 index 99e5749c7..000000000 --- a/docs/src/archive/query/project.md +++ /dev/null @@ -1,68 +0,0 @@ -# Proj - -The `proj` operator represents **projection** and is used to select attributes -(columns) from a table, to rename them, or to create new calculated attributes. - -## Simple projection - -The simple projection selects a subset of attributes of the original table. -However, the primary key attributes are always included. - -Using the [example schema](example-schema.md), let table `department` have attributes -**dept**, *dept_name*, *dept_address*, and *dept_phone*. -The primary key attribute is in bold. - -Then `department.proj()` will have attribute **dept**. - -`department.proj('dept')` will have attribute **dept**. - -`department.proj('dept_name', 'dept_phone')` will have attributes **dept**, -*dept_name*, and *dept_phone*. - -## Renaming - -In addition to selecting attributes, `proj` can rename them. -Any attribute can be renamed, including primary key attributes. - -This is done using keyword arguments: -`tab.proj(new_attr='old_attr')` - -For example, let table `tab` have attributes **mouse**, **session**, *session_date*, -*stimulus*, and *behavior*. -The primary key attributes are in bold. - -Then - -```python -tab.proj(animal='mouse', 'stimulus') -``` - -will have attributes **animal**, **session**, and *stimulus*. - -Renaming is often used to control the outcome of a [join](join.md). -For example, let `tab` have attributes **slice**, and **cell**. -Then `tab * tab` will simply yield `tab`. -However, - -```python -tab * tab.proj(other='cell') -``` - -yields all ordered pairs of all cells in each slice. - -## Calculations - -In addition to selecting or renaming attributes, `proj` can compute new attributes from -existing ones. - -For example, let `tab` have attributes `mouse`, `scan`, `surface_z`, and `scan_z`. -To obtain the new attribute `depth` computed as `scan_z - surface_z` and then to -restrict to `depth > 500`: - -```python -tab.proj(depth='scan_z-surface_z') & 'depth > 500' -``` - -Calculations are passed to SQL and are not parsed by DataJoint. -For available functions, you may refer to the -[MySQL documentation](https://dev.mysql.com/doc/refman/8.0/en/functions.html). diff --git a/docs/src/archive/query/query-caching.md b/docs/src/archive/query/query-caching.md deleted file mode 100644 index 124381b63..000000000 --- a/docs/src/archive/query/query-caching.md +++ /dev/null @@ -1,42 +0,0 @@ -# Query Caching - -Query caching allows avoiding repeated queries to the database by caching the results -locally for faster retrieval. - -To enable queries, set the query cache local path in `dj.config`, create the directory, -and activate the query caching. - -```python -# set the query cache path -dj.config['query_cache'] = os.path.expanduser('~/dj_query_cache') - -# access the active connection object for the tables -conn = dj.conn() # if queries co-located with tables -conn = module.schema.connection # if schema co-located with tables -conn = module.table.connection # most flexible - -# activate query caching for a namespace called 'main' -conn.set_query_cache(query_cache='main') -``` - -The `query_cache` argument is an arbitrary string serving to differentiate cache -states; setting a new value will effectively start a new cache, triggering retrieval of -new values once. - -To turn off query caching, use the following: - -```python -conn.set_query_cache(query_cache=None) -# or -conn.set_query_cache() -``` - -While query caching is enabled, any insert or delete calls and any transactions are -disabled and will raise an error. This ensures that stale data are not used for -updating the database in violation of data integrity. - -To clear and remove the query cache, use the following: - -```python -conn.purge_query_cache() -``` diff --git a/docs/src/archive/query/restrict.md b/docs/src/archive/query/restrict.md deleted file mode 100644 index f8b61e641..000000000 --- a/docs/src/archive/query/restrict.md +++ /dev/null @@ -1,205 +0,0 @@ -# Restriction - -## Restriction operators `&` and `-` - -The restriction operator `A & cond` selects the subset of entities from `A` that meet -the condition `cond`. -The exclusion operator `A - cond` selects the complement of restriction, i.e. the -subset of entities from `A` that do not meet the condition `cond`. - -Restriction and exclusion. - -![Restriction and exclusion](../images/op-restrict.png){: style="width:400px; align:center"} - -The condition `cond` may be one of the following: - -+ another table -+ a mapping, e.g. `dict` -+ an expression in a character string -+ a collection of conditions as a `list`, `tuple`, or Pandas `DataFrame` -+ a Boolean expression (`True` or `False`) -+ an `AndList` -+ a `Not` object -+ a query expression - -As the restriction and exclusion operators are complementary, queries can be -constructed using both operators that will return the same results. -For example, the queries `A & cond` and `A - Not(cond)` will return the same entities. - -## Restriction by a table - -When restricting table `A` with another table, written `A & B`, the two tables must be -**join-compatible** (see `join-compatible` in [Operators](./operators.md)). -The result will contain all entities from `A` for which there exist a matching entity -in `B`. -Exclusion of table `A` with table `B`, or `A - B`, will contain all entities from `A` -for which there are no matching entities in `B`. - -Restriction by another table. - -![Restriction by another table](../images/restrict-example1.png){: style="width:546px; align:center"} - -Exclusion by another table. - -![Exclusion by another table](../images/diff-example1.png){: style="width:539px; align:center"} - -### Restriction by a table with no common attributes - -Restriction of table `A` with another table `B` having none of the same attributes as -`A` will simply return all entities in `A`, unless `B` is empty as described below. -Exclusion of table `A` with `B` having no common attributes will return no entities, -unless `B` is empty as described below. - -Restriction by a table having no common attributes. - -![Restriction by a table with no common attributes](../images/restrict-example2.png){: style="width:571px; align:center"} - -Exclusion by a table having no common attributes. - -![Exclusion by a table having no common attributes](../images/diff-example2.png){: style="width:571px; align:center"} - -### Restriction by an empty table - -Restriction of table `A` with an empty table will return no entities regardless of -whether there are any matching attributes. -Exclusion of table `A` with an empty table will return all entities in `A`. - -Restriction by an empty table. - -![Restriction by an empty table](../images/restrict-example3.png){: style="width:563px; align:center"} - -Exclusion by an empty table. - -![Exclusion by an empty table](../images/diff-example3.png){: style="width:571px; align:center"} - -## Restriction by a mapping - -A key-value mapping may be used as an operand in restriction. -For each key that is an attribute in `A`, the paired value is treated as part of an -equality condition. -Any key-value pairs without corresponding attributes in `A` are ignored. - -Restriction by an empty mapping or by a mapping with no keys matching the attributes in -`A` will return all the entities in `A`. -Exclusion by an empty mapping or by a mapping with no matches will return no entities. - -For example, let's say that table `Session` has the attribute `session_date` of -[datatype](../design/tables/attributes.md) `datetime`. -You are interested in sessions from January 1st, 2018, so you write the following -restriction query using a mapping. - -```python -Session & {'session_date': "2018-01-01"} -``` - -Our mapping contains a typo omitting the final `e` from `session_date`, so no keys in -our mapping will match any attribute in `Session`. -As such, our query will return all of the entities of `Session`. - -## Restriction by a string - -Restriction can be performed when `cond` is an explicit condition on attribute values, -expressed as a string. -Such conditions may include arithmetic operations, functions, range tests, etc. -Restriction of table `A` by a string containing an attribute not found in table `A` -produces an error. - -```python -# All the sessions performed by Alice -Session & 'user = "Alice"' - -# All the experiments at least one minute long -Experiment & 'duration >= 60' -``` - -## Restriction by a collection - -A collection can be a list, a tuple, or a Pandas `DataFrame`. - -```python -# a list: -cond_list = ['first_name = "Aaron"', 'last_name = "Aaronson"'] - -# a tuple: -cond_tuple = ('first_name = "Aaron"', 'last_name = "Aaronson"') - -# a dataframe: -import pandas as pd -cond_frame = pd.DataFrame( - data={'first_name': ['Aaron'], 'last_name': ['Aaronson']}) -``` - -When `cond` is a collection of conditions, the conditions are applied by logical -disjunction (logical OR). -Thus, restriction of table `A` by a collection will return all entities in `A` that -meet *any* of the conditions in the collection. -For example, if you restrict the `Student` table by a collection containing two -conditions, one for a first and one for a last name, your query will return any -students with a matching first name *or* a matching last name. - -```python -Student() & ['first_name = "Aaron"', 'last_name = "Aaronson"'] -``` - -Restriction by a collection, returning all entities matching any condition in the collection. - -![Restriction by collection](../images/python_collection.png){: style="align:center"} - -Restriction by an empty collection returns no entities. -Exclusion of table `A` by an empty collection returns all the entities of `A`. - -## Restriction by a Boolean expression - -`A & True` and `A - False` are equivalent to `A`. - -`A & False` and `A - True` are empty. - -## Restriction by an `AndList` - -The special function `dj.AndList` represents logical conjunction (logical AND). -Restriction of table `A` by an `AndList` will return all entities in `A` that meet -*all* of the conditions in the list. -`A & dj.AndList([c1, c2, c3])` is equivalent to `A & c1 & c2 & c3`. -Usually, it is more convenient to simply write out all of the conditions, as -`A & c1 & c2 & c3`. -However, when a list of conditions has already been generated, the list can simply be -passed as the argument to `dj.AndList`. - -Restriction of table `A` by an empty `AndList`, as in `A & dj.AndList([])`, will return -all of the entities in `A`. -Exclusion by an empty `AndList` will return no entities. - -## Restriction by a `Not` object - -The special function `dj.Not` represents logical negation, such that `A & dj.Not(cond)` -is equivalent to `A - cond`. - -## Restriction by a query - -Restriction by a query object is a generalization of restriction by a table (which is -also a query object), because DataJoint queries always produce well-defined entity -sets, as described in [entity normalization](../design/normalization.md). -As such, restriction by queries follows the same behavior as restriction by tables -described above. - -The example below creates a query object corresponding to all the sessions performed by -the user Alice. -The `Experiment` table is then restricted by the query object, returning all the -experiments that are part of sessions performed by Alice. - -```python -query = Session & 'user = "Alice"' -Experiment & query -``` - -## Restriction by `dj.Top` - -Restriction by `dj.Top` returns the number of entities specified by the `limit` -argument. These entities can be returned in the order specified by the `order_by` -argument. And finally, the `offset` argument can be used to offset the returned entities -which is useful for pagination in web applications. - -```python -# Return the first 10 sessions in descending order of session date -Session & dj.Top(limit=10, order_by='session_date DESC') -``` diff --git a/docs/src/archive/query/union.md b/docs/src/archive/query/union.md deleted file mode 100644 index 71f0fa687..000000000 --- a/docs/src/archive/query/union.md +++ /dev/null @@ -1,48 +0,0 @@ -# Union - -The union operator is not yet implemented -- this page serves as the specification for -the upcoming implementation. -Union is rarely needed in practice. - -## Union operator `+` - -The result of the union operator `A + B` contains all the entities from both operands. -[Entity normalization](../design/normalization.md) requires that the operands in a -union both belong to the same entity type with the same primary key using homologous -attributes. -In the absence of any secondary attributes, the result of a union is the simple set union. - -When secondary attributes are present, they must have the same names and datatypes in -both operands. -The two operands must also be **disjoint**, without any duplicate primary key values -across both inputs. -These requirements prevent ambiguity of attribute values and preserve entity identity. - -## Principles of union - -1. As in all operators, the order of the attributes in the operands is not significant. -2. Operands `A` and `B` must have the same primary key attributes. - Otherwise, an error will be raised. -3. Operands `A` and `B` may not have any common non-key attributes. - Otherwise, an error will be raised. -4. The result `A + B` will have the same primary key as `A` and `B`. -5. The result `A + B` will have all the non-key attributes from both `A` and `B`. -6. For entities that are found in both `A` and `B` (based on the primary key), the -secondary attributes will be filled from the corresponding entities in `A` and `B`. -7. For entities that are only found in either `A` or `B`, the other operand's secondary -attributes will filled with null values. - -## Examples of union - -Example 1 : Note that the order of the attributes does not matter. - -![union-example1](../images/union-example1.png){: style="width:404px; align:center"} - -Example 2 : Non-key attributes are combined from both tables and filled with NULLs when missing. - -![union-example2](../images/union-example2.png){: style="width:539px; align:center"} - -## Properties of union - -1. Commutative: `A + B` is equivalent to `B + A`. -2. Associative: `(A + B) + C` is equivalent to `A + (B + C)`. diff --git a/docs/src/archive/query/universals.md b/docs/src/archive/query/universals.md deleted file mode 100644 index a9f12dd96..000000000 --- a/docs/src/archive/query/universals.md +++ /dev/null @@ -1,46 +0,0 @@ -# Universal Sets - -All [query operators](operators.md) are designed to preserve the entity types of their -inputs. -However, some queries require creating a new entity type that is not represented by any -stored tables. -This means that a new entity type must be explicitly defined as part of the query. -Universal sets fulfill this role. - -**Universal sets** are used in DataJoint to define virtual tables with arbitrary -primary key structures for use in query expressions. -A universal set, defined using class `dj.U`, denotes the set of all possible entities -with given attributes of any possible datatype. -Universal sets allow query expressions using virtual tables when no suitable base table exists. -Attributes of universal sets are allowed to be matched to any namesake attributes, even -those that do not come from the same initial source. - -For example, you may like to query the university database for the complete list of -students' home cities, along with the number of students from each city. -The [schema](example-schema.md) for the university database does not have a table for -cities and states. -A virtual table can fill the role of the nonexistent base table, allowing queries that -would not be possible otherwise. - -```python -# All home cities of students -dj.U('home_city', 'home_state') & Student - -# Total number of students from each city -dj.U('home_city', 'home_state').aggr(Student, n="count(*)") - -# Total number of students from each state -U('home_state').aggr(Student, n="count(*)") - -# Total number of students in the database -U().aggr(Student, n="count(*)") -``` - -The result of aggregation on a universal set is restricted to the entities with matches -in the aggregated table, such as `Student` in the example above. -In other words, `X.aggr(A, ...)` is interpreted as `(X & A).aggr(A, ...)` for universal -set `X`. -All attributes of a universal set are considered primary. - -Universal sets should be used sparingly when no suitable base tables already exist. -In some cases, defining a new base table can make queries clearer and more semantically constrained. diff --git a/docs/src/archive/quick-start.md b/docs/src/archive/quick-start.md deleted file mode 100644 index 17f783405..000000000 --- a/docs/src/archive/quick-start.md +++ /dev/null @@ -1,466 +0,0 @@ -# Quick Start Guide - -## Tutorials - -The easiest way to get started is through the [DataJoint -Tutorials](https://github.com/datajoint/datajoint-tutorials). These tutorials are -configured to run using [GitHub Codespaces](https://github.com/features/codespaces) -where the full environment including the database is already set up. - -Advanced users can install DataJoint locally. Please see the installation instructions below. - -## Installation - -First, please [install Python](https://www.python.org/downloads/) version -3.10 or later. - -Next, please install DataJoint via one of the following: - -=== "conda" - - Pre-Requisites - - Ensure you have [conda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html#regular-installation) - installed. - - To add the `conda-forge` channel: - - ```bash - conda config --add channels conda-forge - ``` - - To install: - - ```bash - conda install -c conda-forge datajoint - ``` - -=== "pip + :fontawesome-brands-windows:" - - Pre-Requisites - - Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed. - - Install [graphviz](https://graphviz.org/download/#windows) pre-requisite for - diagram visualization. - - To install: - - ```bash - pip install datajoint - ``` - -=== "pip + :fontawesome-brands-apple:" - - Pre-Requisites - - Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed. - - Install [graphviz](https://graphviz.org/download/#mac) pre-requisite for - diagram visualization. - - To install: - - ```bash - pip install datajoint - ``` - -=== "pip + :fontawesome-brands-linux:" - - Pre-Requisites - - Ensure you have [pip](https://pip.pypa.io/en/stable/installation/) installed. - - Install [graphviz](https://graphviz.org/download/#linux) pre-requisite for - diagram visualization. - - To install: - - ```bash - pip install datajoint - ``` - -## Connection - -=== "environment variables" - - Before using `datajoint`, set the following environment variables like so: - - ```bash linenums="1" - DJ_HOST={host_address} - DJ_USER={user} - DJ_PASS={password} - ``` - -=== "memory" - - To set connection settings within Python, perform: - - ```python linenums="1" - import datajoint as dj - - dj.config.database.host = "{host_address}" - dj.config.database.user = "{user}" - dj.config.database.password = "{password}" - ``` - - Note: Credentials set this way are not persisted. For persistent configuration, - use environment variables or a config file. - -=== "file" - - Create a file named `datajoint.json` in your project root: - - ```json linenums="1" - { - "database": { - "host": "{host_address}" - } - } - ``` - - **Important:** Never store credentials in config files. Use environment variables - (`DJ_USER`, `DJ_PASS`) or a `.secrets/` directory instead. - - DataJoint searches for `datajoint.json` starting from the current directory and - moving up through parent directories until it finds the file or reaches a `.git` - directory. - -## Data Pipeline Definition - -Let's definite a simple data pipeline. - -```python linenums="1" -import datajoint as dj -schema = dj.Schema(f"{dj.config['database.user']}_shapes") # This statement creates the database schema `{username}_shapes` on the server. - -@schema # The `@schema` decorator for DataJoint classes creates the table on the server. -class Rectangle(dj.Manual): - definition = """ # The table is defined by the the `definition` property. - shape_id: int - --- - shape_height: float - shape_width: float - """ - -@schema -class Area(dj.Computed): - definition = """ - -> Rectangle - --- - shape_area: float - """ - def make(self, key): - rectangle = (Rectangle & key).fetch1() - Area.insert1( - dict( - shape_id=rectangle["shape_id"], - shape_area=rectangle["shape_height"] * rectangle["shape_width"], - ) - ) -``` - -It is a common practice to have a separate Python module for each schema. Therefore, -each such module has only one `dj.Schema` object defined and is usually named -`schema`. - -The `dj.Schema` constructor can take a number of optional parameters -after the schema name. - -- `context` - Dictionary for looking up foreign key references. - Defaults to `None` to use local context. -- `connection` - Specifies the DataJoint connection object. Defaults - to `dj.conn()`. -- `create_schema` - When `False`, the schema object will not create a - schema on the database and will raise an error if one does not - already exist. Defaults to `True`. -- `create_tables` - When `False`, the schema object will not create - tables on the database and will raise errors when accessing missing - tables. Defaults to `True`. - -The `@schema` decorator uses the class name and the data tier to check whether an -appropriate table exists on the database. If a table does not already exist, the -decorator creates one on the database using the definition property. The decorator -attaches the information about the table to the class, and then returns the class. - -## Diagram - -### Display - -The diagram displays the relationship of the data model in the data pipeline. - -This can be done for an entire schema: - -```python -import datajoint as dj -schema = dj.Schema('my_database') -dj.Diagram(schema) -``` - -![pipeline](./images/shapes_pipeline.svg) - -Or for individual or sets of tables: -```python -dj.Diagram(schema.Rectangle) -dj.Diagram(schema.Rectangle) + dj.Diagram(schema.Area) -``` - -What if I don't see the diagram? - -Some Python interfaces may require additional `draw` method. - -```python -dj.Diagram(schema).draw() -``` - -Calling the `.draw()` method is not necessary when working in a Jupyter notebook by -entering `dj.Diagram(schema)` in a notebook cell. The Diagram will automatically -render in the notebook by calling its `_repr_html_` method. A Diagram displayed -without `.draw()` will be rendered as an SVG, and hovering the mouse over a table -will reveal a compact version of the output of the `.describe()` method. - -For more information about diagrams, see [this article](../design/diagrams). - -### Customize - -Adding or subtracting a number to a diagram object adds nodes downstream or upstream, -respectively, in the pipeline. - -```python -(dj.Diagram(schema.Rectangle)+1).draw() # Plot all the tables directly downstream from `schema.Rectangle` -``` - -```python -(dj.Diagram('my_schema')-1+1).draw() # Plot all tables directly downstream of those directly upstream of this schema. -``` - -### Save - -The diagram can be saved as either `png` or `svg`. - -```python -dj.Diagram(schema).save(filename='my-diagram', format='png') -``` - -## Insert data - -Data entry is as easy as providing the appropriate data structure to a permitted -[table](./design/tables/tiers.md). - -Let's add data for a rectangle: - -```python -Rectangle.insert1(dict(shape_id=1, shape_height=2, shape_width=4)) -``` - -Given the following [table definition](./design/tables/declare.md), we can insert data -as tuples, dicts, pandas dataframes, or pathlib `Path` relative paths to local CSV -files. - -```python -mouse_id: int # unique mouse id ---- -dob: date # mouse date of birth -sex: enum('M', 'F', 'U') # sex of mouse - Male, Female, or Unknown -``` - -=== "Tuple" - - ```python - mouse.insert1( (0, '2017-03-01', 'M') ) # Single entry - data = [ - (1, '2016-11-19', 'M'), - (2, '2016-11-20', 'U'), - (5, '2016-12-25', 'F') - ] - mouse.insert(data) # Multi-entry - ``` - -=== "Dict" - - ```python - mouse.insert1( dict(mouse_id=0, dob='2017-03-01', sex='M') ) # Single entry - data = [ - {'mouse_id':1, 'dob':'2016-11-19', 'sex':'M'}, - {'mouse_id':2, 'dob':'2016-11-20', 'sex':'U'}, - {'mouse_id':5, 'dob':'2016-12-25', 'sex':'F'} - ] - mouse.insert(data) # Multi-entry - ``` - -=== "Pandas" - - ```python - import pandas as pd - data = pd.DataFrame( - [[1, "2016-11-19", "M"], [2, "2016-11-20", "U"], [5, "2016-12-25", "F"]], - columns=["mouse_id", "dob", "sex"], - ) - mouse.insert(data) - ``` - -=== "CSV" - - Given the following CSV in the current working directory as `mice.csv` - - ```console - mouse_id,dob,sex - 1,2016-11-19,M - 2,2016-11-20,U - 5,2016-12-25,F - ``` - - We can import as follows: - - ```python - from pathlib import Path - mouse.insert(Path('./mice.csv')) - ``` - -## Run computation - -Let's start the computations on our entity: `Area`. - -```python -Area.populate(display_progress=True) -``` - -The `make` method populates automated tables from inserted data. Read more in the -full article [here](./compute/make.md) - -## Query - -Let's inspect the results. - -```python -Area & "shape_area >= 8" -``` - -| shaped_id | shape_area | -| --- | --- | -| 1 | 8.0 | - -## Fetch - -Data queries in DataJoint comprise two distinct steps: - -1. Construct the `query` object to represent the required data using - tables and [operators](../query/operators). -2. Fetch the data from `query` into the workspace of the host language. - -Note that entities returned by `fetch` methods are not guaranteed to be sorted in any -particular order unless specifically requested. Furthermore, the order is not -guaranteed to be the same in any two queries, and the contents of two identical queries -may change between two sequential invocations unless they are wrapped in a transaction. -Therefore, if you wish to fetch matching pairs of attributes, do so in one `fetch` -call. - -```python -data = query.fetch() -``` - -### Entire table - -A `fetch` command can either retrieve table data as a NumPy -[recarray](https://docs.scipy.org/doc/numpy/reference/generated/numpy.recarray.html) -or a as a list of `dict` - -```python -data = query.fetch() # NumPy recarray -data = query.fetch(as_dict=True) # List of `dict` -``` - -In some cases, the amount of data returned by fetch can be quite large; it can be -useful to use the `size_on_disk` attribute to determine if running a bare fetch -would be wise. Please note that it is only currently possible to query the size of -entire tables stored directly in the database at this time. - -### Separate variables - -```python -name, img = query.fetch1('mouse_id', 'dob') # when query has exactly one entity -name, img = query.fetch('mouse_id', 'dob') # [mouse_id, ...] [dob, ...] -``` - -### Primary key values - -```python -keydict = tab.fetch1("KEY") # single key dict when tab has exactly one entity -keylist = tab.fetch("KEY") # list of key dictionaries [{}, ...] -``` - -`KEY` can also used when returning attribute values as separate -variables, such that one of the returned variables contains the entire -primary keys. - -### Sorting results - -To sort the result, use the `order_by` keyword argument. - -```python -data = query.fetch(order_by='mouse_id') # ascending order -data = query.fetch(order_by='mouse_id desc') # descending order -data = query.fetch(order_by=('mouse_id', 'dob')) # by ID first, dob second -data = query.fetch(order_by='KEY') # sort by the primary key -``` - -The `order_by` argument can be a string specifying the attribute to sort by. By default -the sort is in ascending order. Use `'attr desc'` to sort in descending order by -attribute `attr`. The value can also be a sequence of strings, in which case, the sort -performed on all the attributes jointly in the order specified. - -The special attribute named `'KEY'` represents the primary key attributes in order that -they appear in the index. Otherwise, this name can be used as any other argument. - -If an attribute happens to be a SQL reserved word, it needs to be enclosed in -backquotes. For example: - -```python -data = query.fetch(order_by='`select` desc') -``` - -The `order_by` value is eventually passed to the `ORDER BY` -[clause](https://dev.mysql.com/doc/refman/8.0/en/order-by-optimization.html). - -### Limiting results - -Similar to sorting, the `limit` and `offset` arguments can be used to limit the result -to a subset of entities. - -```python -data = query.fetch(order_by='mouse_id', limit=10, offset=5) -``` - -Note that an `offset` cannot be used without specifying a `limit` as -well. - -### Usage with Pandas - -The `pandas` [library](http://pandas.pydata.org/) is a popular library for data analysis -in Python which can easily be used with DataJoint query results. Since the records -returned by `fetch()` are contained within a `numpy.recarray`, they can be easily -converted to `pandas.DataFrame` objects by passing them into the `pandas.DataFrame` -constructor. For example: - -```python -import pandas as pd -frame = pd.DataFrame(tab.fetch()) -``` - -Calling `fetch()` with the argument `format="frame"` returns results as -`pandas.DataFrame` objects indexed by the table's primary key attributes. - -```python -frame = tab.fetch(format="frame") -``` - -Returning results as a `DataFrame` is not possible when fetching a particular subset of -attributes or when `as_dict` is set to `True`. - -## Drop - -The `drop` method completely removes a table from the database, including its -definition. It also removes all dependent tables, recursively. DataJoint will first -display the tables being dropped and the number of entities in each before prompting -the user for confirmation to proceed. - -The `drop` method is often used during initial design to allow altered -table definitions to take effect. - -```python -# drop the Person table from its schema -Person.drop() -``` diff --git a/docs/src/archive/sysadmin/bulk-storage.md b/docs/src/archive/sysadmin/bulk-storage.md deleted file mode 100644 index 12af44791..000000000 --- a/docs/src/archive/sysadmin/bulk-storage.md +++ /dev/null @@ -1,104 +0,0 @@ -# Bulk Storage Systems - -## Why External Bulk Storage? - -DataJoint supports the storage of large data objects associated with -relational records externally from the MySQL Database itself. This is -significant and useful for a number of reasons. - -### Cost - -One reason is that the high-performance storage commonly used in database systems is -more expensive than typical commodity storage. Therefore, storing the smaller identifying -information typically used in queries on fast, relational database storage and storing -the larger bulk data used for analysis or processing on lower cost commodity storage -enables large savings in storage expense. - -### Flexibility - -Storing bulk data separately also facilitates more flexibility in -usage, since the bulk data can managed using separate maintenance -processes than those in the relational storage. - -For example, larger relational databases may require many hours to be -restored in the event of system failures. If the relational portion of -the data is stored separately, with the larger bulk data stored on -another storage system, this downtime can be reduced to a matter of -minutes. Similarly, due to the lower cost of bulk commodity storage, -more emphasis can be put into redundancy of this data and backups to -help protect the non-relational data. - -### Performance - -Storing the non-relational bulk data separately can have system -performance impacts by removing data transfer, disk I/O, and memory -load from the database server and shifting these to the bulk storage -system. Additionally, DataJoint supports caching of bulk data records -which can allow for faster processing of records which already have -been retrieved in previous queries. - -### Data Sharing - -DataJoint provides pluggable support for different external bulk storage backends, -allowing data sharing by publishing bulk data to S3-Protocol compatible data shares both -in the cloud and on locally managed systems and other common tools for data sharing, -such as Globus, etc. - -## Bulk Storage Scenarios - -Typical bulk storage considerations relate to the cost of the storage -backend per unit of storage, the amount of data which will be stored, -the desired focus of the shared data (system performance, data -flexibility, data sharing), and data access. Some common scenarios are -given in the following table: - -| Scenario | Storage Solution | System Requirements | Notes | -| -- | -- | -- | -- | -| Local Object Cache | Local External Storage | Local Hard Drive | Used to Speed Access to other Storage | -| LAN Object Cache | Network External Storage | Local Network Share | Used to Speed Access to other storage, reduce Cloud/Network Costs/Overhead | -| Local Object Store | Local/Network External Storage | Local/Network Storage | Used to store objects externally from the database | -| Local S3-Compatible Store | Local S3-Compatible Server | Network S3-Server | Used to host S3-Compatible services locally (e.g. minio) for internal use or to lower cloud costs | -| Cloud S3-Compatible Storage | Cloud Provider | Internet Connectivity | Used to reduce/remove requirement for external storage management, data sharing | -| Globus Storage | Globus Endpoint | Local/Local Network Storage, Internet Connectivity | Used for institutional data transfer or publishing. | - -## Bulk Storage Considerations - -Although external bulk storage provides a variety of advantages for -storage cost and data sharing, it also uses slightly different data -input/retrieval semantics and as such has different performance -characteristics. - -### Performance Characteristics - -In the direct database connection scenario, entire result sets are -either added or retrieved from the database in a single stream -action. In the case of external storage, individual record components -are retrieved in a set of sequential actions per record, each one -subject to the network round trip to the given storage medium. As -such, tables using many small records may be ill suited to external -storage usage in the absence of a caching mechanism. While some of -these impacts may be addressed by code changes in a future release of -DataJoint, to some extent, the impact is directly related from needing -to coordinate the activities of the database data stream with the -external storage system, and so cannot be avoided. - -### Network Traffic - -Some of the external storage solutions mentioned above incur cost both -at a data volume and transfer bandwidth level. The number of users -querying the database, data access, and use of caches should be -considered in these cases to reduce this cost if applicable. - -### Data Coherency - -When storing all data directly in the relational data store, it is -relatively easy to ensure that all data in the database is consistent -in the event of system issues such as crash recoveries, since MySQL’s -relational storage engine manages this for you. When using external -storage however, it is important to ensure that any data recoveries of -the database system are paired with a matching point-in-time of the -external storage system. While DataJoint does use hashing to help -facilitate a guarantee that external files are uniquely named -throughout their lifecycle, the pairing of a given relational dataset -against a given filesystem state is loosely coupled, and so an -incorrect pairing could result in processing failures or other issues. diff --git a/docs/src/archive/sysadmin/database-admin.md b/docs/src/archive/sysadmin/database-admin.md deleted file mode 100644 index 352a3af11..000000000 --- a/docs/src/archive/sysadmin/database-admin.md +++ /dev/null @@ -1,364 +0,0 @@ -# Database Administration - -## Hosting - -Let’s say a person, a lab, or a multi-lab consortium decide to use DataJoint as their -data pipeline platform. -What IT resources and support will be required? - -DataJoint uses a MySQL-compatible database server such as MySQL, MariaDB, Percona -Server, or Amazon Aurora to store the structured data used for all relational -operations. -Large blocks of data associated with these records such as multidimensional numeric -arrays (signals, images, scans, movies, etc) can be stored within the database or -stored in additionally configured [bulk storage](../client/stores.md). - -The first decisions you need to make are where this server will be hosted and how it -will be administered. -The server may be hosted on your personal computer, on a dedicated machine in your lab, -or in a cloud-based database service. - -### Cloud hosting - -Increasingly, many teams make use of cloud-hosted database services, which allow great -flexibility and easy administration of the database server. -A cloud hosting option will be provided through https://works.datajoint.com. -DataJoint Works simplifies the setup for labs that wish to host their data pipelines in -the cloud and allows sharing pipelines between multiple groups and locations. -Being an open-source solution, other cloud services such as Amazon RDS can also be used -in this role, albeit with less DataJoint-centric customization. - -### Self hosting - -In the most basic configuration, the relational database management system (database -server) is installed on an individual user's personal computer. -To support a group of users, a specialized machine can be configured as a dedicated -database server. -This server can be accessed by multiple DataJoint clients to query the data and perform -computations. - -For larger groups and multi-site collaborations with heavy workloads, the database -server cluster may be configured in the cloud or on premises. -The following section provides some basic guidelines for these configurations here and -in the subsequent sections of the documentation. - -### General server / hardware support requirements - -The following table lists some likely scenarios for DataJoint database server -deployments and some reasonable estimates of the required computer hardware. -The required IT/systems support needed to ensure smooth operations in the absence of -local database expertise is also listed. - -#### IT infrastructures - -| Usage Scenario | DataJoint Database Computer | Required IT Support | -| -- | -- | -- | -| Single User | Personal Laptop or Workstation | Self-Supported or Ad-Hoc General IT Support | -| Small Group (e.g. 2-10 Users) | Workstation or Small Server | Ad-Hoc General or Experienced IT Support | -| Medium Group (e.g. 10-30 Users) | Small to Medium Server | Ad-Hoc/Part Time Experienced or Specialized IT Support | -| Large Group/Department (e.g. 30-50+ Users) | Medium/Large Server or Multi-Server Replication | Part Time/Dedicated Experienced or Specialized IT Support | -| Multi-Location Collaboration (30+ users, Geographically Distributed) | Large Server, Advanced Replication | Dedicated Specialized IT Support | - -## Configuration - -### Hardware considerations - -As in any computer system, CPU, RAM memory, disk storage, and network speed are -important components of performance. -The relational database component of DataJoint is no exception to this rule. -This section discusses the various factors relating to selecting a server for your -DataJoint pipelines. - -#### CPU - -CPU speed and parallelism (number of cores/threads) will impact the speed of queries -and the number of simultaneous queries which can be efficiently supported by the system. -It is a good rule of thumb to have enough cores to support the number of active users -and background tasks you expect to have running during a typical 'busy' day of usage. -For example, a team of 10 people might want to have 8 cores to support a few active -queries and background tasks. - -#### RAM - -The amount of RAM will impact the amount of DataJoint data kept in memory, allowing for -faster querying of data since the data can be searched and returned to the user without -needing to access the slower disk drives. -It is a good idea to get enough memory to fully store the more important and frequently -accessed portions of your dataset with room to spare, especially if in-database blob -storage is used instead of external [bulk storage](bulk-storage.md). - -#### Disk - -The disk storage for a DataJoint database server should have fast random access, -ideally with flash-based storage to eliminate the rotational delay of mechanical hard -drives. - -#### Networking - -When network connections are used, network speed and latency are important to ensure -that large query results can be quickly transferred across the network and that delays -due to data entry/query round-trip have minimal impact on the runtime of the program. - -#### General recommendations - -DataJoint datasets can consist of many thousands or even millions of records. -Generally speaking one would want to make sure that the relational database system has -sufficient CPU speed and parallelism to support a typical number of concurrent users -and to execute searches quickly. -The system should have enough RAM to store the primary key values of commonly used -tables and operating system caches. -Disk storage should be fast enough to support quick loading of and searching through -the data. -Lastly, network bandwidth must be sufficient to support transferring user records -quickly. - -### Large-scale installations - -Database replication may be beneficial if system downtime or precise database -responsiveness is a concern -Replication can allow for easier coordination of maintenance activities, faster -recovery in the event of system problems, and distribution of the database workload -across server machines to increase throughput and responsiveness. - -#### Multi-master replication - -Multi-master replication configurations allow for all replicas to be used in a read/ -write fashion, with the workload being distributed among all machines. -However, multi-master replication is also more complicated, requiring front-end -machines to distribute the workload, similar performance characteristics on all -replicas to prevent bottlenecks, and redundant network connections to ensure the -replicated machines are always in sync. - -### Recommendations - -It is usually best to go with the simplest solution which can suit the requirements of -the installation, adjusting workloads where possible and adding complexity only as -needs dictate. - -Resource requirements of course depend on the data collection and processing needs of -the given pipeline, but there are general size guidelines that can inform any system -configuration decisions. -A reasonably powerful workstation or small server should support the needs of a small -group (2-10 users). -A medium or large server should support the needs of a larger user community (10-30 -users). -A replicated or distributed setup of 2 or more medium or large servers may be required -in larger cases. -These requirements can be reduced through the use of external or cloud storage, which -is discussed in the subsequent section. - -| Usage Scenario | DataJoint Database Computer | Hardware Recommendation | -| -- | -- | -- | -| Single User | Personal Laptop or Workstation | 4 Cores, 8-16GB or more of RAM, SSD or better storage | -| Small Group (e.g. 2-10 Users) | Workstation or Small Server | 8 or more Cores, 16GB or more of RAM, SSD or better storage | -| Medium Group (e.g. 10-30 Users) | Small to Medium Server | 8-16 or more Cores, 32GB or more of RAM, SSD/RAID or better storage | -| Large Group/Department (e.g. 30-50+ Users) | Medium/Large Server or Multi-Server Replication | 16-32 or more Cores, 64GB or more of RAM, SSD Raid storage, multiple machines | -| Multi-Location Collaboration (30+ users, Geographically Distributed) | Large Server, Advanced Replication | 16-32 or more Cores, 64GB or more of RAM, SSD Raid storage, multiple machines; potentially multiple machines in multiple locations | - -### Docker - -A Docker image is available for a MySQL server configured to work with DataJoint: https://github.com/datajoint/mysql-docker. - -## User Management - -Create user accounts on the MySQL server. For example, if your -username is alice, the SQL code for this step is: - -```mysql -CREATE USER 'alice'@'%' IDENTIFIED BY 'alices-secret-password'; -``` - -Existing users can be listed using the following SQL: - -```mysql -SELECT user, host from mysql.user; -``` - -Teams that use DataJoint typically divide their data into schemas -grouped together by common prefixes. For example, a lab may have a -collection of schemas that begin with `common_`. Some common -processing may be organized into several schemas that begin with -`pipeline_`. Typically each user has all privileges to schemas that -begin with their username. - -For example, alice may have privileges to select and insert data from -the common schemas (but not create new tables), and have all -privileges to the pipeline schemas. - -Then the SQL code to grant her privileges might look like: - -```mysql -GRANT SELECT, INSERT ON `common\_%`.* TO 'alice'@'%'; -GRANT ALL PRIVILEGES ON `pipeline\_%`.* TO 'alice'@'%'; -GRANT ALL PRIVILEGES ON `alice\_%`.* TO 'alice'@'%'; -``` - -To note, the ```ALL PRIVILEGES``` option allows the user to create -and remove databases without administrator intervention. - -Once created, a user's privileges can be listed using the ```SHOW GRANTS``` -statement. - -```mysql -SHOW GRANTS FOR 'alice'@'%'; -``` - -### Grouping with Wildcards - -Depending on the complexity of your installation, using additional -wildcards to group access rules together might make managing user -access rules simpler. For example, the following equivalent -convention: - -```mysql -GRANT ALL PRIVILEGES ON `user_alice\_%`.* TO 'alice'@'%'; -``` - -Could then facilitate using a rule like: - -```mysql -GRANT SELECT ON `user\_%\_%`.* TO 'bob'@'%'; -``` - -to enable `bob` to query all other users tables using the -`user_username_database` convention without needing to explicitly -give him access to `alice\_%`, `charlie\_%`, and so on. - -This convention can be further expanded to create notions of groups -and protected schemas for background processing, etc. For example: - -```mysql -GRANT ALL PRIVILEGES ON `group\_shared\_%`.* TO 'alice'@'%'; -GRANT ALL PRIVILEGES ON `group\_shared\_%`.* TO 'bob'@'%'; - -GRANT ALL PRIVILEGES ON `group\_wonderland\_%`.* TO 'alice'@'%'; -GRANT SELECT ON `group\_wonderland\_%`.* TO 'alice'@'%'; -``` - -could allow both bob an alice to read/write into the -```group\_shared``` databases, but in the case of the -```group\_wonderland``` databases, read write access is restricted -to alice. - -## Backups and Recovery - -Backing up your DataJoint installation is critical to ensuring that your work is safe -and can be continued in the event of system failures, and several mechanisms are -available to use. - -Much like your live installation, your backup will consist of two portions: - -- Backup of the Relational Data -- Backup of optional external bulk storage - -This section primarily deals with backup of the relational data since most of the -optional bulk storage options use "regular" flat-files for storage and can be backed up -via any "normal" disk backup regime. - -There are many options to backup MySQL; subsequent sections discuss a few options. - -### Cloud hosted backups - -In the case of cloud-hosted options, many cloud vendors provide automated backup of -your data, and some facility for downloading such backups externally. -Due to the wide variety of cloud-specific options, discussion of these options falls -outside of the scope of this documentation. -However, since the cloud server is also a MySQL server, other options listed here may -work for your situation. - -### Disk-based backup - -The simplest option for many cases is to perform a disk-level backup of your MySQL -installation using standard disk backup tools. -It should be noted that all database activity should be stopped for the duration of the -backup to prevent errors with the backed up data. -This can be done in one of two ways: - -- Stopping the MySQL server program -- Using database locks - -These methods are required since MySQL data operations can be ongoing in the background -even when no user activity is ongoing. -To use a database lock to perform a backup, the following commands can be used as the -MySQL administrator: - -```mysql -FLUSH TABLES WITH READ LOCK; -UNLOCK TABLES; -``` - -The backup should be performed between the issuing of these two commands, ensuring the -database data is consistent on disk when it is backed up. - -### MySQLDump - -Disk based backups may not be feasible for every installation, or a database may -require constant activity such that stopping it for backups is not feasible. -In such cases, the simplest option is -[MySQLDump](https://dev.mysql.com/doc/mysql-backup-excerpt/8.0/en/using-mysqldump.html), - a command line tool that prints the contents of your database contents in SQL form. - -This tool is generally acceptable for most cases and is especially well suited for -smaller installations due to its simplicity and ease of use. - -For larger installations, the lower speed of MySQLDump can be a limitation, since it -has to convert the database contents to and from SQL rather than dealing with the -database files directly. -Additionally, since backups are performed within a transaction, the backup will be -valid up to the time the backup began rather than to its completion, which can make -ensuring that the latest data are fully backed up more difficult as the time it takes -to run a backup grows. - -### Percona XTraBackup - -The Percona `xtrabackup` tool provides near-realtime backup capability of a MySQL -installation, with extended support for replicated databases, and is a good tool for -backing up larger databases. - -However, this tool requires local disk access as well as reasonably fast backup media, -since it builds an ongoing transaction log in real time to ensure that backups are -valid up to the point of their completion. -This strategy fails if it cannot keep up with the write speed of the database. -Further, the backups it generates are in binary format and include incomplete database -transactions, which require careful attention to detail when restoring. - -As such, this solution is recommended only for advanced use cases or larger databases -where limitations of the other solutions may apply. - -### Locking and DDL issues - -One important thing to note is that at the time of writing, MySQL's transactional -system is not `data definition language` aware, meaning that changes to table -structures occurring during some backup schemes can result in corrupted backup copies. -If schema changes will be occurring during your backup window, it is a good idea to -ensure that appropriate locking mechanisms are used to prevent these changes during -critical steps of the backup process. - -However, on busy installations which cannot be stopped, the use of locks in many backup -utilities may cause issues if your programs expect to write data to the database during -the backup window. - -In such cases it might make sense to review the given backup tools for locking related -options or to use other mechanisms such as replicas or alternate backup tools to -prevent interaction of the database. - -### Replication and snapshots for backup - -Larger databases consisting of many Terabytes of data may take many hours or even days -to backup and restore, and so downtime resulting from system failure can create major -impacts to ongoing work. - -While not backup tools per-se, use of MySQL replication and disk snapshots -can be useful to assist in reducing the downtime resulting from a full database outage. - -Replicas can be configured so that one copy of the data is immediately online in the -event of server crash. -When a server fails in this case, users and programs simply restart and point to the -new server before resuming work. - -Replicas can also reduce the system load generated by regular backup procedures, since -they can be backed up instead of the main server. -Additionally they can allow more flexibility in a given backup scheme, such as allowing -for disk snapshots on a busy system that would not otherwise be able to be stopped. -A replica copy can be stopped temporarily and then resumed while a disk snapshot or -other backup operation occurs. diff --git a/docs/src/archive/sysadmin/external-store.md b/docs/src/archive/sysadmin/external-store.md deleted file mode 100644 index aac61fe24..000000000 --- a/docs/src/archive/sysadmin/external-store.md +++ /dev/null @@ -1,293 +0,0 @@ -# External Store - -DataJoint organizes most of its data in a relational database. -Relational databases excel at representing relationships between entities and storing -structured data. -However, relational databases are not particularly well-suited for storing large -continuous chunks of data such as images, signals, and movies. -An attribute of type `longblob` can contain an object up to 4 GiB in size (after -compression) but storing many such large objects may hamper the performance of queries -on the entire table. -A good rule of thumb is that objects over 10 MiB in size should not be put in the -relational database. -In addition, storing data in cloud-hosted relational databases (e.g. AWS RDS) may be -more expensive than in cloud-hosted simple storage systems (e.g. AWS S3). - -DataJoint allows the use of `external` storage to store large data objects within its -relational framework but outside of the main database. - -Defining an externally-stored attribute is used using the notation `blob@storename` -(see also: [definition syntax](../design/tables/declare.md)) and works the same way as -a `longblob` attribute from the users perspective. However, its data are stored in an -external storage system rather than in the relational database. - -Various systems can play the role of external storage, including a shared file system -accessible to all team members with access to these objects or a cloud storage -solutions such as AWS S3. - -For example, the following table stores motion-aligned two-photon movies. - -```python -# Motion aligned movies --> twophoton.Scan ---- -aligned_movie : blob@external # motion-aligned movie in 'external' store -``` - -All [insert](../manipulation/insert.md) and [fetch](../query/fetch.md) operations work -identically for `external` attributes as they do for `blob` attributes, with the same -serialization protocol. -Similar to `blobs`, `external` attributes cannot be used in restriction conditions. - -Multiple external storage configurations may be used simultaneously with the -`@storename` portion of the attribute definition determining the storage location. - -```python -# Motion aligned movies --> twophoton.Scan ---- -aligned_movie : blob@external-raw # motion-aligned movie in 'external-raw' store -``` - -## Principles of operation - -External storage is organized to emulate individual attribute values in the relational -database. -DataJoint organizes external storage to preserve the same data integrity principles as -in relational storage. - -1. The external storage locations are specified in the DataJoint connection -configuration with one specification for each store. - - ```python - dj.config['stores'] = { - 'external': dict( # 'regular' external storage for this pipeline - protocol='s3', - endpoint='s3.amazonaws.com:9000', - bucket = 'testbucket', - location = 'datajoint-projects/lab1', - access_key='1234567', - secret_key='foaf1234'), - 'external-raw': dict( # 'raw' storage for this pipeline - protocol='file', - location='/net/djblobs/myschema') - } - # external object cache - see fetch operation below for details. - dj.config['cache'] = '/net/djcache' - ``` - -2. Each schema corresponds to a dedicated folder at the storage location with the same -name as the database schema. - -3. Stored objects are identified by the [SHA-256](https://en.wikipedia.org/wiki/SHA-2) -hashes (in web-safe base-64 ASCII) of their serialized contents. - This scheme allows for the same object—used multiple times in the same schema—to be - stored only once. - -4. In the `external-raw` storage, the objects are saved as files with the hash as the -filename. - -5. In the `external` storage, external files are stored in a directory layout -corresponding to the hash of the filename. By default, this corresponds to the first 2 -characters of the hash, followed by the second 2 characters of the hash, followed by -the actual file. - -6. Each database schema has an auxiliary table named `~external_` for each -configured external store. - - It is automatically created the first time external storage is used. - The primary key of `~external_` is the hash of the data (for blobs and - attachments) or of the relative paths to the files for filepath-based storage. - Other attributes are the `count` of references by tables in the schema, the `size` - of the object in bytes, and the `timestamp` of the last event (creation, update, or - deletion). - - Below are sample entries in `~external_`. - - | HASH | size | filepath | contents_hash | timestamp | - | -- | -- | -- | -- | -- | - | 1GEqtEU6JYEOLS4sZHeHDxWQ3JJfLlH VZio1ga25vd2 | 1039536788 | NULL | NULL | 2017-06-07 23:14:01 | - - The fields `filepath` and `contents_hash` relate to the - [filepath](../design/tables/filepath.md) datatype, which will be discussed - separately. - -7. Attributes of type `@` are declared as renamed -[foreign keys](../design/tables/dependencies.md) referencing the -`~external_` table (but are not shown as such to the user). - -8. The [insert](../manipulation/insert.md) operation encodes and hashes the blob data. -If an external object is not present in storage for the same hash, the object is saved -and if the save operation is successful, corresponding entities in table -`~external_` for that store are created. - -9. The [delete](../manipulation/delete.md) operation first deletes the foreign key -reference in the target table. The external table entry and actual external object is -not actually deleted at this time (`soft-delete`). - -10. The [fetch](../query/fetch.md) operation uses the hash values to find the data. - In order to prevent excessive network overhead, a special external store named - `cache` can be configured. - If the `cache` is enabled, the `fetch` operation need not access - `~external_` directly. - Instead `fetch` will retrieve the cached object without downloading directly from - the `real` external store. - -11. Cleanup is performed regularly when the database is in light use or off-line. - -12. DataJoint never removes objects from the local `cache` folder. - The `cache` folder may just be periodically emptied entirely or based on file - access date. - If dedicated `cache` folders are maintained for each schema, then a special - procedure will be provided to remove all objects that are no longer listed in - `~external_`. - -Data removal from external storage is separated from the delete operations to ensure -that data are not lost in race conditions between inserts and deletes of the same -objects, especially in cases of transactional processing or in processes that are -likely to get terminated. -The cleanup steps are performed in a separate process when the risks of race conditions -are minimal. -The process performing the cleanups must be isolated to prevent interruptions resulting -in loss of data integrity. - -## Configuration - -The following steps must be performed to enable external storage: - -1. Assign external location settings for each storage as shown in the -[Step 1](#principles-of-operation) example above. Use `dj.config` for configuration. - - - `protocol` [`s3`, `file`] Specifies whether `s3` or `file` external storage is - desired. - - `endpoint` [`s3`] Specifies the remote endpoint to the external data for all - schemas as well as the target port. - - `bucket` [`s3`] Specifies the appropriate `s3` bucket organization. - - `location` [`s3`, `file`] Specifies the subdirectory within the root or bucket of - store to preserve data. External objects are thus stored remotely with the following - path structure: - `////`. - - `access_key` [`s3`] Specifies the access key credentials for accessing the external - location. - - `secret_key` [`s3`] Specifies the secret key credentials for accessing the external - location. - - `secure` [`s3`] Optional specification to establish secure external storage - connection with TLS (aka SSL, HTTPS). Defaults to `False`. - -2. Optionally, for each schema specify the `cache` folder for local fetch cache. - - This is done by saving the path in the `cache` key of the DataJoint configuration - dictionary: - - ```python - dj.config['cache'] = '/temp/dj-cache' - ``` - -## Cleanup - -Deletion of records containing externally stored blobs is a `soft-delete` which only -removes the database-side records from the database. -To cleanup the external tracking table or the actual external files, a separate process -is provided as follows. - -To remove only the tracking entries in the external table, call `delete` -on the `~external_` table for the external configuration with the argument -`delete_external_files=False`. - -Note: Currently, cleanup operations on a schema's external table are not 100% - transaction safe and so must be run when there is no write activity occurring - in tables which use a given schema / external store pairing. - -```python -schema.external['external_raw'].delete(delete_external_files=False) -``` - -To remove the tracking entries as well as the underlying files, call `delete` -on the external table for the external configuration with the argument -`delete_external_files=True`. - -```python -schema.external['external_raw'].delete(delete_external_files=True) -``` - -Note: Setting `delete_external_files=True` will always attempt to delete - the underlying data file, and so should not typically be used with - the `filepath` datatype. - -## Migration between DataJoint v0.11 and v0.12 - -Note: Please read carefully if you have used external storage in DataJoint v0.11! - -The initial implementation of external storage was reworked for -DataJoint v0.12. These changes are backward-incompatible with DataJoint -v0.11 so care should be taken when upgrading. This section outlines -some details of the change and a general process for upgrading to a -format compatible with DataJoint v0.12 when a schema rebuild is not -desired. - -The primary changes to the external data implementation are: - -- The external object tracking mechanism was modified. Tracking tables -were extended for additional external datatypes and split into -per-store tables to improve database performance in schemas with -many external objects. - -- The external storage format was modified to use a nested subfolder -structure (`folding`) to improve performance and interoperability -with some filesystems that have limitations or performance problems -when storing large numbers of files in single directories. - -Depending on the circumstances, the simplest way to migrate data to -v0.12 may be to drop and repopulate the affected schemas. This will construct -the schema and storage structure in the v0.12 format and save the need for -database migration. When recreation is not possible or is not preferred -to upgrade to DataJoint v0.12, the following process should be followed: - - 1. Stop write activity to all schemas using external storage. - - 2. Perform a full backup of your database(s). - - 3. Upgrade your DataJoint installation to v0.12 - - 4. Adjust your external storage configuration (in `datajoint.config`) - to the new v0.12 configuration format (see above). - - 5. Migrate external tracking tables for each schema to use the new format. For - instance in Python: - - ```python - import datajoint.migrate as migrate - db_schema_name='schema_1' - external_store='raw' - migrate.migrate_dj011_external_blob_storage_to_dj012(db_schema_name, external_store) - ``` - - 6. Verify pipeline functionality after this process has completed. For instance in - Python: - - ```python - x = myschema.TableWithExternal.fetch('external_field', limit=1)[0] - ``` - -Note: This migration function is provided on a best-effort basis, and will - convert the external tracking tables into a format which is compatible - with DataJoint v0.12. While we have attempted to ensure correctness - of the process, all use-cases have not been heavily tested. Please be sure to fully - back-up your data and be prepared to investigate problems with the - migration, should they occur. - -Please note: - -- The migration only migrates the tracking table format and does not -modify the backing file structure to support `folding`. The DataJoint -v0.12 logic is able to work with this format, but to take advantage -of the new backend storage, manual adjustment of the tracking table -and files, or a full rebuild of the schema should be performed. - -- Additional care to ensure all clients are using v0.12 should be -taken after the upgrade. Legacy clients may incorrectly create data -in the old format which would then need to be combined or otherwise -reconciled with the data in v0.12 format. You might wish to take -the opportunity to version-pin your installations so that future -changes requiring controlled upgrades can be coordinated on a system -wide basis. diff --git a/docs/src/archive/tutorials/dj-top.ipynb b/docs/src/archive/tutorials/dj-top.ipynb deleted file mode 100644 index 5920a9f25..000000000 --- a/docs/src/archive/tutorials/dj-top.ipynb +++ /dev/null @@ -1,1015 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Using the dj.Top restriction" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "First you will need to [install](../../getting-started/#installation) and [connect](../../getting-started/#connection) to a DataJoint [data pipeline](https://docs.datajoint.com/core/datajoint-python/latest/concepts/data-pipelines/#what-is-a-data-pipeline).\n", - "\n", - "Now let's start by importing the `datajoint` client." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[2024-12-20 11:10:20,120][INFO]: Connecting root@127.0.0.1:3306\n", - "[2024-12-20 11:10:20,259][INFO]: Connected root@127.0.0.1:3306\n" - ] - } - ], - "source": [ - "import datajoint as dj\n", - "\n", - "dj.config[\"database.host\"] = \"127.0.0.1\"\n", - "schema = dj.Schema(\"university\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Table Definition" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "@schema\n", - "class Student(dj.Manual):\n", - " definition = \"\"\"\n", - " student_id : int unsigned # university-wide ID number\n", - " ---\n", - " first_name : varchar(40)\n", - " last_name : varchar(40)\n", - " sex : enum('F', 'M', 'U')\n", - " date_of_birth : date\n", - " home_address : varchar(120) # mailing street address\n", - " home_city : varchar(60) # mailing address\n", - " home_state : char(2) # US state acronym: e.g. OH\n", - " home_zip : char(10) # zipcode e.g. 93979-4979\n", - " home_phone : varchar(20) # e.g. 414.657.6883x0881\n", - " \"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "@schema\n", - "class Department(dj.Manual):\n", - " definition = \"\"\"\n", - " dept : varchar(6) # abbreviated department name, e.g. BIOL\n", - " ---\n", - " dept_name : varchar(200) # full department name\n", - " dept_address : varchar(200) # mailing address\n", - " dept_phone : varchar(20)\n", - " \"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "@schema\n", - "class StudentMajor(dj.Manual):\n", - " definition = \"\"\"\n", - " -> Student\n", - " ---\n", - " -> Department\n", - " declare_date : date # when student declared her major\n", - " \"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "@schema\n", - "class Course(dj.Manual):\n", - " definition = \"\"\"\n", - " -> Department\n", - " course : int unsigned # course number, e.g. 1010\n", - " ---\n", - " course_name : varchar(200) # e.g. \"Neurobiology of Sensation and Movement.\"\n", - " credits : decimal(3,1) # number of credits earned by completing the course\n", - " \"\"\"\n", - "\n", - "\n", - "@schema\n", - "class Term(dj.Manual):\n", - " definition = \"\"\"\n", - " term_year : year\n", - " term : enum('Spring', 'Summer', 'Fall')\n", - " \"\"\"\n", - "\n", - "\n", - "@schema\n", - "class Section(dj.Manual):\n", - " definition = \"\"\"\n", - " -> Course\n", - " -> Term\n", - " section : char(1)\n", - " ---\n", - " auditorium : varchar(12)\n", - " \"\"\"\n", - "\n", - "\n", - "@schema\n", - "class CurrentTerm(dj.Manual):\n", - " definition = \"\"\"\n", - " -> Term\n", - " \"\"\"\n", - "\n", - "\n", - "@schema\n", - "class Enroll(dj.Manual):\n", - " definition = \"\"\"\n", - " -> Student\n", - " -> Section\n", - " \"\"\"\n", - "\n", - "\n", - "@schema\n", - "class LetterGrade(dj.Lookup):\n", - " definition = \"\"\"\n", - " grade : char(2)\n", - " ---\n", - " points : decimal(3,2)\n", - " \"\"\"\n", - " contents = [\n", - " [\"A\", 4.00],\n", - " [\"A-\", 3.67],\n", - " [\"B+\", 3.33],\n", - " [\"B\", 3.00],\n", - " [\"B-\", 2.67],\n", - " [\"C+\", 2.33],\n", - " [\"C\", 2.00],\n", - " [\"C-\", 1.67],\n", - " [\"D+\", 1.33],\n", - " [\"D\", 1.00],\n", - " [\"F\", 0.00],\n", - " ]\n", - "\n", - "\n", - "@schema\n", - "class Grade(dj.Manual):\n", - " definition = \"\"\"\n", - " -> Enroll\n", - " ---\n", - " -> LetterGrade\n", - " \"\"\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Insert" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from tqdm import tqdm\n", - "import faker\n", - "import random\n", - "import datetime\n", - "\n", - "fake = faker.Faker()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "def yield_students():\n", - " fake_name = {\"F\": fake.name_female, \"M\": fake.name_male}\n", - " while True: # ignore invalid values\n", - " try:\n", - " sex = random.choice((\"F\", \"M\"))\n", - " first_name, last_name = fake_name[sex]().split(\" \")[:2]\n", - " street_address, city = fake.address().split(\"\\n\")\n", - " city, state = city.split(\", \")\n", - " state, zipcode = state.split(\" \")\n", - " except ValueError:\n", - " continue\n", - " else:\n", - " yield dict(\n", - " first_name=first_name,\n", - " last_name=last_name,\n", - " sex=sex,\n", - " home_address=street_address,\n", - " home_city=city,\n", - " home_state=state,\n", - " home_zip=zipcode,\n", - " date_of_birth=str(fake.date_time_between(start_date=\"-35y\", end_date=\"-15y\").date()),\n", - " home_phone=fake.phone_number()[:20],\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "Student.insert(dict(k, student_id=i) for i, k in zip(range(100, 300), yield_students()))\n", - "\n", - "Department.insert(\n", - " dict(\n", - " dept=dept,\n", - " dept_name=name,\n", - " dept_address=fake.address(),\n", - " dept_phone=fake.phone_number()[:20],\n", - " )\n", - " for dept, name in [\n", - " [\"CS\", \"Computer Science\"],\n", - " [\"BIOL\", \"Life Sciences\"],\n", - " [\"PHYS\", \"Physics\"],\n", - " [\"MATH\", \"Mathematics\"],\n", - " ]\n", - ")\n", - "\n", - "StudentMajor.insert(\n", - " {**s, **d, \"declare_date\": fake.date_between(start_date=datetime.date(1999, 1, 1))}\n", - " for s, d in zip(Student.fetch(\"KEY\"), random.choices(Department.fetch(\"KEY\"), k=len(Student())))\n", - " if random.random() < 0.75\n", - ")\n", - "\n", - "# from https://www.utah.edu/\n", - "Course.insert(\n", - " [\n", - " [\"BIOL\", 1006, \"World of Dinosaurs\", 3],\n", - " [\"BIOL\", 1010, \"Biology in the 21st Century\", 3],\n", - " [\"BIOL\", 1030, \"Human Biology\", 3],\n", - " [\"BIOL\", 1210, \"Principles of Biology\", 4],\n", - " [\"BIOL\", 2010, \"Evolution & Diversity of Life\", 3],\n", - " [\"BIOL\", 2020, \"Principles of Cell Biology\", 3],\n", - " [\"BIOL\", 2021, \"Principles of Cell Science\", 4],\n", - " [\"BIOL\", 2030, \"Principles of Genetics\", 3],\n", - " [\"BIOL\", 2210, \"Human Genetics\", 3],\n", - " [\"BIOL\", 2325, \"Human Anatomy\", 4],\n", - " [\"BIOL\", 2330, \"Plants & Society\", 3],\n", - " [\"BIOL\", 2355, \"Field Botany\", 2],\n", - " [\"BIOL\", 2420, \"Human Physiology\", 4],\n", - " [\"PHYS\", 2040, \"Classcal Theoretical Physics II\", 4],\n", - " [\"PHYS\", 2060, \"Quantum Mechanics\", 3],\n", - " [\"PHYS\", 2100, \"General Relativity and Cosmology\", 3],\n", - " [\"PHYS\", 2140, \"Statistical Mechanics\", 4],\n", - " [\"PHYS\", 2210, \"Physics for Scientists and Engineers I\", 4],\n", - " [\"PHYS\", 2220, \"Physics for Scientists and Engineers II\", 4],\n", - " [\"PHYS\", 3210, \"Physics for Scientists I (Honors)\", 4],\n", - " [\"PHYS\", 3220, \"Physics for Scientists II (Honors)\", 4],\n", - " [\"MATH\", 1250, \"Calculus for AP Students I\", 4],\n", - " [\"MATH\", 1260, \"Calculus for AP Students II\", 4],\n", - " [\"MATH\", 1210, \"Calculus I\", 4],\n", - " [\"MATH\", 1220, \"Calculus II\", 4],\n", - " [\"MATH\", 2210, \"Calculus III\", 3],\n", - " [\"MATH\", 2270, \"Linear Algebra\", 4],\n", - " [\"MATH\", 2280, \"Introduction to Differential Equations\", 4],\n", - " [\"MATH\", 3210, \"Foundations of Analysis I\", 4],\n", - " [\"MATH\", 3220, \"Foundations of Analysis II\", 4],\n", - " [\"CS\", 1030, \"Foundations of Computer Science\", 3],\n", - " [\"CS\", 1410, \"Introduction to Object-Oriented Programming\", 4],\n", - " [\"CS\", 2420, \"Introduction to Algorithms & Data Structures\", 4],\n", - " [\"CS\", 2100, \"Discrete Structures\", 3],\n", - " [\"CS\", 3500, \"Software Practice\", 4],\n", - " [\"CS\", 3505, \"Software Practice II\", 3],\n", - " [\"CS\", 3810, \"Computer Organization\", 4],\n", - " [\"CS\", 4400, \"Computer Systems\", 4],\n", - " [\"CS\", 4150, \"Algorithms\", 3],\n", - " [\"CS\", 3100, \"Models of Computation\", 3],\n", - " [\"CS\", 3200, \"Introduction to Scientific Computing\", 3],\n", - " [\"CS\", 4000, \"Senior Capstone Project - Design Phase\", 3],\n", - " [\"CS\", 4500, \"Senior Capstone Project\", 3],\n", - " [\"CS\", 4940, \"Undergraduate Research\", 3],\n", - " [\"CS\", 4970, \"Computer Science Bachelors Thesis\", 3],\n", - " ]\n", - ")\n", - "\n", - "Term.insert(dict(term_year=year, term=term) for year in range(1999, 2019) for term in [\"Spring\", \"Summer\", \"Fall\"])\n", - "\n", - "Term().fetch(order_by=(\"term_year DESC\", \"term DESC\"), as_dict=True, limit=1)[0]\n", - "\n", - "CurrentTerm().insert1({**Term().fetch(order_by=(\"term_year DESC\", \"term DESC\"), as_dict=True, limit=1)[0]})\n", - "\n", - "\n", - "def make_section(prob):\n", - " for c in (Course * Term).proj():\n", - " for sec in \"abcd\":\n", - " if random.random() < prob:\n", - " break\n", - " yield {\n", - " **c,\n", - " \"section\": sec,\n", - " \"auditorium\": random.choice(\"ABCDEF\") + str(random.randint(1, 100)),\n", - " }\n", - "\n", - "\n", - "Section.insert(make_section(0.5))" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 200/200 [00:27<00:00, 7.17it/s]\n" - ] - } - ], - "source": [ - "# Enrollment\n", - "terms = Term().fetch(\"KEY\")\n", - "quit_prob = 0.1\n", - "for student in tqdm(Student.fetch(\"KEY\")):\n", - " start_term = random.randrange(len(terms))\n", - " for term in terms[start_term:]:\n", - " if random.random() < quit_prob:\n", - " break\n", - " else:\n", - " sections = ((Section & term) - (Course & (Enroll & student))).fetch(\"KEY\")\n", - " if sections:\n", - " Enroll.insert(\n", - " {**student, **section} for section in random.sample(sections, random.randrange(min(5, len(sections))))\n", - " )\n", - "\n", - "# assign random grades\n", - "grades = LetterGrade.fetch(\"grade\")\n", - "\n", - "grade_keys = Enroll.fetch(\"KEY\")\n", - "random.shuffle(grade_keys)\n", - "grade_keys = grade_keys[: len(grade_keys) * 9 // 10]\n", - "\n", - "Grade.insert({**key, \"grade\": grade} for key, grade in zip(grade_keys, random.choices(grades, k=len(grade_keys))))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# dj.Top Restriction" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "

student_id

\n", - " university-wide ID number\n", - "
\n", - "

dept

\n", - " abbreviated department name, e.g. BIOL\n", - "
\n", - "

course

\n", - " course number, e.g. 1010\n", - "
\n", - "

term_year

\n", - " \n", - "
\n", - "

term

\n", - " \n", - "
\n", - "

section

\n", - " \n", - "
\n", - "

grade

\n", - " \n", - "
\n", - "

points

\n", - " \n", - "
100MATH22802018FallaA-3.67
191MATH22102018SpringbA4.00
211CS21002018FallaA4.00
273PHYS21002018SpringaA4.00
282BIOL20212018SpringdA4.00
\n", - " \n", - "

Total: 5

\n", - " " - ], - "text/plain": [ - "*student_id *dept *course *term_year *term *section *grade points \n", - "+------------+ +------+ +--------+ +-----------+ +--------+ +---------+ +-------+ +--------+\n", - "100 MATH 2280 2018 Fall a A- 3.67 \n", - "191 MATH 2210 2018 Spring b A 4.00 \n", - "211 CS 2100 2018 Fall a A 4.00 \n", - "273 PHYS 2100 2018 Spring a A 4.00 \n", - "282 BIOL 2021 2018 Spring d A 4.00 \n", - " (Total: 5)" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "(Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(limit=5, order_by=\"points DESC\", offset=5)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"SELECT `grade`,`student_id`,`dept`,`course`,`term_year`,`term`,`section`,`points` FROM `university`.`#letter_grade` NATURAL JOIN `university`.`grade` WHERE ( (term_year='2018')) ORDER BY `points` DESC LIMIT 10\"" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "((LetterGrade * Grade) & \"term_year='2018'\" & dj.Top(limit=10, order_by=\"points DESC\", offset=0)).make_sql()" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"SELECT `student_id`,`dept`,`course`,`term_year`,`term`,`section`,`grade`,`points` FROM `university`.`grade` NATURAL JOIN `university`.`#letter_grade` WHERE ( (term_year='2018')) ORDER BY `points` DESC LIMIT 20\"" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "((Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(limit=20, order_by=\"points DESC\", offset=0)).make_sql()" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "

student_id

\n", - " university-wide ID number\n", - "
\n", - "

dept

\n", - " abbreviated department name, e.g. BIOL\n", - "
\n", - "

course

\n", - " course number, e.g. 1010\n", - "
\n", - "

term_year

\n", - " \n", - "
\n", - "

term

\n", - " \n", - "
\n", - "

section

\n", - " \n", - "
\n", - "

grade

\n", - " \n", - "
\n", - "

points

\n", - " \n", - "
100CS32002018FallcA4.00
100MATH22802018FallaA-3.67
100PHYS22102018SpringdA4.00
122CS10302018FallcB+3.33
131BIOL20302018SpringaA4.00
131CS32002018FallbB+3.33
136BIOL22102018SpringcB+3.33
136MATH22102018FallbB+3.33
141BIOL20102018SummercB+3.33
141CS24202018FallbA4.00
141CS32002018FallbA-3.67
182CS14102018SummercA-3.67
\n", - "

...

\n", - "

Total: 20

\n", - " " - ], - "text/plain": [ - "*student_id *dept *course *term_year *term *section *grade points \n", - "+------------+ +------+ +--------+ +-----------+ +--------+ +---------+ +-------+ +--------+\n", - "100 CS 3200 2018 Fall c A 4.00 \n", - "100 MATH 2280 2018 Fall a A- 3.67 \n", - "100 PHYS 2210 2018 Spring d A 4.00 \n", - "122 CS 1030 2018 Fall c B+ 3.33 \n", - "131 BIOL 2030 2018 Spring a A 4.00 \n", - "131 CS 3200 2018 Fall b B+ 3.33 \n", - "136 BIOL 2210 2018 Spring c B+ 3.33 \n", - "136 MATH 2210 2018 Fall b B+ 3.33 \n", - "141 BIOL 2010 2018 Summer c B+ 3.33 \n", - "141 CS 2420 2018 Fall b A 4.00 \n", - "141 CS 3200 2018 Fall b A- 3.67 \n", - "182 CS 1410 2018 Summer c A- 3.67 \n", - " ...\n", - " (Total: 20)" - ] - }, - "execution_count": 47, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "(Grade * LetterGrade) & \"term_year='2018'\" & dj.Top(limit=20, order_by=\"points DESC\", offset=0)" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "

grade

\n", - " \n", - "
\n", - "

student_id

\n", - " university-wide ID number\n", - "
\n", - "

dept

\n", - " abbreviated department name, e.g. BIOL\n", - "
\n", - "

course

\n", - " course number, e.g. 1010\n", - "
\n", - "

term_year

\n", - " \n", - "
\n", - "

term

\n", - " \n", - "
\n", - "

section

\n", - " \n", - "
\n", - "

points

\n", - " \n", - "
A100CS32002018Fallc4.00
A100PHYS22102018Springd4.00
A131BIOL20302018Springa4.00
A141CS24202018Fallb4.00
A186PHYS22102018Springa4.00
A191MATH22102018Springb4.00
A211CS21002018Falla4.00
A273PHYS21002018Springa4.00
A282BIOL20212018Springd4.00
A-100MATH22802018Falla3.67
A-141CS32002018Fallb3.67
A-182CS14102018Summerc3.67
\n", - "

...

\n", - "

Total: 20

\n", - " " - ], - "text/plain": [ - "*grade *student_id *dept *course *term_year *term *section points \n", - "+-------+ +------------+ +------+ +--------+ +-----------+ +--------+ +---------+ +--------+\n", - "A 100 CS 3200 2018 Fall c 4.00 \n", - "A 100 PHYS 2210 2018 Spring d 4.00 \n", - "A 131 BIOL 2030 2018 Spring a 4.00 \n", - "A 141 CS 2420 2018 Fall b 4.00 \n", - "A 186 PHYS 2210 2018 Spring a 4.00 \n", - "A 191 MATH 2210 2018 Spring b 4.00 \n", - "A 211 CS 2100 2018 Fall a 4.00 \n", - "A 273 PHYS 2100 2018 Spring a 4.00 \n", - "A 282 BIOL 2021 2018 Spring d 4.00 \n", - "A- 100 MATH 2280 2018 Fall a 3.67 \n", - "A- 141 CS 3200 2018 Fall b 3.67 \n", - "A- 182 CS 1410 2018 Summer c 3.67 \n", - " ...\n", - " (Total: 20)" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "(LetterGrade * Grade) & \"term_year='2018'\" & dj.Top(limit=20, order_by=\"points DESC\", offset=0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "elements", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/src/archive/tutorials/json.ipynb b/docs/src/archive/tutorials/json.ipynb deleted file mode 100644 index 9c5feebf6..000000000 --- a/docs/src/archive/tutorials/json.ipynb +++ /dev/null @@ -1,1080 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "7fe24127-c0d0-4ff8-96b4-6ab0d9307e73", - "metadata": {}, - "source": [ - "# Using the json type" - ] - }, - { - "cell_type": "markdown", - "id": "62450023", - "metadata": {}, - "source": [ - "> ⚠️ Note the following before using the `json` type\n", - "> - Supported only for MySQL >= 8.0 when [JSON_VALUE](https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-value) introduced.\n", - "> - Equivalent Percona is fully-compatible.\n", - "> - MariaDB is not supported since [JSON_VALUE](https://mariadb.com/kb/en/json_value/#syntax) does not allow type specification like MySQL's.\n", - "> - Not yet supported in DataJoint MATLAB" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "67cf93d2", - "metadata": {}, - "source": [ - "First you will need to [install](../../getting-started/#installation) and [connect](../../getting-started/#connection) to a DataJoint [data pipeline](https://docs.datajoint.com/core/datajoint-python/latest/concepts/data-pipelines/#what-is-a-data-pipeline).\n", - "\n", - "Now let's start by importing the `datajoint` client." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bc0b6f54-8f11-45f4-bf8d-e1058ee0056f", - "metadata": {}, - "outputs": [], - "source": [ - "import datajoint as dj" - ] - }, - { - "cell_type": "markdown", - "id": "3544cab9-f2db-458a-9431-939bea5affc5", - "metadata": {}, - "source": [ - "## Table Definition" - ] - }, - { - "cell_type": "markdown", - "id": "a2998c71", - "metadata": {}, - "source": [ - "For this exercise, let's imagine we work for an awesome company that is organizing a fun RC car race across various teams in the company. Let's see which team has the fastest car! 🏎️\n", - "\n", - "This establishes 2 important entities: a `Team` and a `Car`. Normally the entities are mapped to their own dedicated table, however, let's assume that `Team` is well-structured but `Car` is less structured than we'd prefer. In other words, the structure for what makes up a *car* is varying too much between entries (perhaps because users of the pipeline haven't agreed yet on the definition? 🤷).\n", - "\n", - "This would make it a good use-case to keep `Team` as a table but make `Car` a `json` type defined within the `Team` table.\n", - "\n", - "Let's begin." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "dc318298-b819-4f06-abbd-7bb7544dd431", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[2023-02-12 00:14:33,027][INFO]: Connecting root@fakeservices.datajoint.io:3306\n", - "[2023-02-12 00:14:33,039][INFO]: Connected root@fakeservices.datajoint.io:3306\n" - ] - } - ], - "source": [ - "schema = dj.Schema(f\"{dj.config['database.user']}_json\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4aaf96db-85d9-4e94-a4c3-3558f4cc6671", - "metadata": {}, - "outputs": [], - "source": [ - "@schema\n", - "class Team(dj.Lookup):\n", - " definition = \"\"\"\n", - " # A team within a company\n", - " name: varchar(40) # team name\n", - " ---\n", - " car=null: json # A car belonging to a team (null to allow registering first but specifying car later)\n", - " \n", - " unique index(car.length:decimal(4, 1)) # Add an index if this key is frequently accessed\n", - " \"\"\"" - ] - }, - { - "cell_type": "markdown", - "id": "640bf7a7-9e07-4953-9c8a-304e55c467f8", - "metadata": {}, - "source": [ - "## Insert" - ] - }, - { - "cell_type": "markdown", - "id": "7081e577", - "metadata": {}, - "source": [ - "Let's suppose that engineering is first up to register their car." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "30f0d62e", - "metadata": {}, - "outputs": [], - "source": [ - "Team.insert1(\n", - " {\n", - " \"name\": \"engineering\",\n", - " \"car\": {\n", - " \"name\": \"Rever\",\n", - " \"length\": 20.5,\n", - " \"inspected\": True,\n", - " \"tire_pressure\": [32, 31, 33, 34],\n", - " \"headlights\": [\n", - " {\n", - " \"side\": \"left\",\n", - " \"hyper_white\": None,\n", - " },\n", - " {\n", - " \"side\": \"right\",\n", - " \"hyper_white\": None,\n", - " },\n", - " ],\n", - " },\n", - " }\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "ee5e4dcf", - "metadata": {}, - "source": [ - "Next, business and marketing teams are up and register their cars.\n", - "\n", - "A few points to notice below:\n", - "- The person signing up on behalf of marketing does not know the specifics of the car during registration but another team member will be updating this soon before the race.\n", - "- Notice how the `business` and `engineering` teams appear to specify the same property but refer to it as `safety_inspected` and `inspected` respectfully." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b532e16c", - "metadata": {}, - "outputs": [], - "source": [ - "Team.insert(\n", - " [\n", - " {\n", - " \"name\": \"marketing\",\n", - " \"car\": None,\n", - " },\n", - " {\n", - " \"name\": \"business\",\n", - " \"car\": {\n", - " \"name\": \"Chaching\",\n", - " \"length\": 100,\n", - " \"safety_inspected\": False,\n", - " \"tire_pressure\": [34, 30, 27, 32],\n", - " \"headlights\": [\n", - " {\n", - " \"side\": \"left\",\n", - " \"hyper_white\": True,\n", - " },\n", - " {\n", - " \"side\": \"right\",\n", - " \"hyper_white\": True,\n", - " },\n", - " ],\n", - " },\n", - " },\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "57365de7", - "metadata": {}, - "source": [ - "We can preview the table data much like normal but notice how the value of `car` behaves like other BLOB-like attributes." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0e3b517c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " A team within a company\n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car

\n", - " A car belonging to a team (null to allow registering first but specifying car later)\n", - "
marketing=BLOB=
engineering=BLOB=
business=BLOB=
\n", - " \n", - "

Total: 3

\n", - " " - ], - "text/plain": [ - "*name car \n", - "+------------+ +--------+\n", - "marketing =BLOB= \n", - "engineering =BLOB= \n", - "business =BLOB= \n", - " (Total: 3)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Team()" - ] - }, - { - "cell_type": "markdown", - "id": "c95cbbee-4ef7-4870-ad42-a60345a3644f", - "metadata": {}, - "source": [ - "## Restriction" - ] - }, - { - "cell_type": "markdown", - "id": "8b454996", - "metadata": {}, - "source": [ - "Now let's see what kinds of queries we can form to demostrate how we can query this pipeline." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "81efda24", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " A team within a company\n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car

\n", - " A car belonging to a team (null to allow registering first but specifying car later)\n", - "
business=BLOB=
\n", - " \n", - "

Total: 1

\n", - " " - ], - "text/plain": [ - "*name car \n", - "+----------+ +--------+\n", - "business =BLOB= \n", - " (Total: 1)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Which team has a `car` equal to 100 inches long?\n", - "Team & {\"car.length\": 100}" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "fd7b855d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " A team within a company\n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car

\n", - " A car belonging to a team (null to allow registering first but specifying car later)\n", - "
engineering=BLOB=
\n", - " \n", - "

Total: 1

\n", - " " - ], - "text/plain": [ - "*name car \n", - "+------------+ +--------+\n", - "engineering =BLOB= \n", - " (Total: 1)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Which team has a `car` less than 50 inches long?\n", - "Team & \"car->>'$.length' < 50\"" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "b76ebb75", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " A team within a company\n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car

\n", - " A car belonging to a team (null to allow registering first but specifying car later)\n", - "
engineering=BLOB=
\n", - " \n", - "

Total: 1

\n", - " " - ], - "text/plain": [ - "*name car \n", - "+------------+ +--------+\n", - "engineering =BLOB= \n", - " (Total: 1)" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Any team that has had their car inspected?\n", - "Team & [{\"car.inspected:unsigned\": True}, {\"car.safety_inspected:unsigned\": True}]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "b787784c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " A team within a company\n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car

\n", - " A car belonging to a team (null to allow registering first but specifying car later)\n", - "
engineering=BLOB=
marketing=BLOB=
\n", - " \n", - "

Total: 2

\n", - " " - ], - "text/plain": [ - "*name car \n", - "+------------+ +--------+\n", - "engineering =BLOB= \n", - "marketing =BLOB= \n", - " (Total: 2)" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Which teams do not have hyper white lights for their first head light?\n", - "Team & {\"car.headlights[0].hyper_white\": None}" - ] - }, - { - "cell_type": "markdown", - "id": "5bcf0b5d", - "metadata": {}, - "source": [ - "Notice that the previous query will satisfy the `None` check if it experiences any of the following scenarious:\n", - "- if entire record missing (`marketing` satisfies this)\n", - "- JSON key is missing\n", - "- JSON value is set to JSON `null` (`engineering` satisfies this)" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "bcf1682e-a0c7-4c2f-826b-0aec9052a694", - "metadata": {}, - "source": [ - "## Projection" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "daea110e", - "metadata": {}, - "source": [ - "Projections can be quite useful with the `json` type since we can extract out just what we need. This allows greater query flexibility but more importantly, for us to be able to fetch only what is pertinent." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "8fb8334a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car_name

\n", - " calculated attribute\n", - "
\n", - "

car_length

\n", - " calculated attribute\n", - "
businessChaching100
engineeringRever20.5
marketingNoneNone
\n", - " \n", - "

Total: 3

\n", - " " - ], - "text/plain": [ - "*name car_name car_length \n", - "+------------+ +----------+ +------------+\n", - "business Chaching 100 \n", - "engineering Rever 20.5 \n", - "marketing None None \n", - " (Total: 3)" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Only interested in the car names and the length but let the type be inferred\n", - "q_untyped = Team.proj(\n", - " car_name=\"car.name\",\n", - " car_length=\"car.length\",\n", - ")\n", - "q_untyped" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "bb5f0448", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'business', 'car_name': 'Chaching', 'car_length': '100'},\n", - " {'name': 'engineering', 'car_name': 'Rever', 'car_length': '20.5'},\n", - " {'name': 'marketing', 'car_name': None, 'car_length': None}]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "q_untyped.fetch(as_dict=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "a307dfd7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - " \n", - " \n", - " \n", - " \n", - "
\n", - " \n", - " \n", - " \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - "

name

\n", - " team name\n", - "
\n", - "

car_name

\n", - " calculated attribute\n", - "
\n", - "

car_length

\n", - " calculated attribute\n", - "
businessChaching100.0
engineeringRever20.5
marketingNoneNone
\n", - " \n", - "

Total: 3

\n", - " " - ], - "text/plain": [ - "*name car_name car_length \n", - "+------------+ +----------+ +------------+\n", - "business Chaching 100.0 \n", - "engineering Rever 20.5 \n", - "marketing None None \n", - " (Total: 3)" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Nevermind, I'll specify the type explicitly\n", - "q_typed = Team.proj(\n", - " car_name=\"car.name\",\n", - " car_length=\"car.length:float\",\n", - ")\n", - "q_typed" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "8a93dbf9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'name': 'business', 'car_name': 'Chaching', 'car_length': 100.0},\n", - " {'name': 'engineering', 'car_name': 'Rever', 'car_length': 20.5},\n", - " {'name': 'marketing', 'car_name': None, 'car_length': None}]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "q_typed.fetch(as_dict=True)" - ] - }, - { - "cell_type": "markdown", - "id": "62dd0239-fa70-4369-81eb-3d46c5053fee", - "metadata": {}, - "source": [ - "## Describe" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "73d9df01", - "metadata": {}, - "source": [ - "Lastly, the `.describe()` function on the `Team` table can help us generate the table's definition. This is useful if we are connected directly to the pipeline without the original source." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "0e739932", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "# A team within a company\n", - "name : varchar(40) # team name\n", - "---\n", - "car=null : json # A car belonging to a team (null to allow registering first but specifying car later)\n", - "UNIQUE INDEX ((json_value(`car`, _utf8mb4'$.length' returning decimal(4, 1))))\n", - "\n" - ] - } - ], - "source": [ - "rebuilt_definition = Team.describe()\n", - "print(rebuilt_definition)" - ] - }, - { - "cell_type": "markdown", - "id": "be1070d5-765b-4bc2-92de-8a6ffd885984", - "metadata": {}, - "source": [ - "## Cleanup" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "id": "cb959927", - "metadata": {}, - "source": [ - "Finally, let's clean up what we created in this tutorial." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "d9cc28a3-3ffd-4126-b7e9-bc6365040b93", - "metadata": {}, - "outputs": [], - "source": [ - "schema.drop()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "68ad4340", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "all_purposes", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.18" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/datajoint/codecs.py b/src/datajoint/codecs.py index cf2e2105f..4ddb33d9c 100644 --- a/src/datajoint/codecs.py +++ b/src/datajoint/codecs.py @@ -112,7 +112,7 @@ def __init_subclass__(cls, *, register: bool = True, **kwargs): existing = _codec_registry[cls.name] if type(existing) is not cls: raise DataJointError( - f"Codec <{cls.name}> already registered by " f"{type(existing).__module__}.{type(existing).__name__}" + f"Codec <{cls.name}> already registered by {type(existing).__module__}.{type(existing).__name__}" ) return # Same class, idempotent @@ -301,7 +301,7 @@ def get_codec(name: str) -> Codec: return _codec_registry[type_name] raise DataJointError( - f"Unknown codec: <{type_name}>. " f"Ensure the codec is defined (inherit from dj.Codec with name='{type_name}')." + f"Unknown codec: <{type_name}>. Ensure the codec is defined (inherit from dj.Codec with name='{type_name}')." ) @@ -499,7 +499,7 @@ def lookup_codec(codec_spec: str) -> tuple[Codec, str | None]: if is_codec_registered(type_name): return get_codec(type_name), store_name - raise DataJointError(f"Codec <{type_name}> is not registered. " "Define a Codec subclass with name='{type_name}'.") + raise DataJointError(f"Codec <{type_name}> is not registered. Define a Codec subclass with name='{{type_name}}'.") # ============================================================================= diff --git a/src/datajoint/content_registry.py b/src/datajoint/content_registry.py index f5da65ff5..70b38324a 100644 --- a/src/datajoint/content_registry.py +++ b/src/datajoint/content_registry.py @@ -151,7 +151,7 @@ def get_content(content_hash: str, store_name: str | None = None) -> bytes: # Verify hash (optional but recommended for integrity) actual_hash = compute_content_hash(data) if actual_hash != content_hash: - raise DataJointError(f"Content hash mismatch: expected {content_hash[:16]}..., " f"got {actual_hash[:16]}...") + raise DataJointError(f"Content hash mismatch: expected {content_hash[:16]}..., got {actual_hash[:16]}...") return data diff --git a/src/datajoint/heading.py b/src/datajoint/heading.py index 96e01f985..96383170b 100644 --- a/src/datajoint/heading.py +++ b/src/datajoint/heading.py @@ -41,17 +41,17 @@ def name(self) -> str: def get_dtype(self, is_external: bool) -> str: raise DataJointError( - f"Codec <{self._codec_name}> is not registered. " f"Define a Codec subclass with name='{self._codec_name}'." + f"Codec <{self._codec_name}> is not registered. Define a Codec subclass with name='{self._codec_name}'." ) def encode(self, value, *, key=None, store_name=None): raise DataJointError( - f"Codec <{self._codec_name}> is not registered. " f"Define a Codec subclass with name='{self._codec_name}'." + f"Codec <{self._codec_name}> is not registered. Define a Codec subclass with name='{self._codec_name}'." ) def decode(self, stored, *, key=None): raise DataJointError( - f"Codec <{self._codec_name}> is not registered. " f"Define a Codec subclass with name='{self._codec_name}'." + f"Codec <{self._codec_name}> is not registered. Define a Codec subclass with name='{self._codec_name}'." ) diff --git a/src/datajoint/jobs.py b/src/datajoint/jobs.py index 7be80a0e5..70c24f354 100644 --- a/src/datajoint/jobs.py +++ b/src/datajoint/jobs.py @@ -145,7 +145,7 @@ def _generate_definition(self) -> str: if not pk_attrs: raise DataJointError( - f"Cannot create jobs table for {self._target.full_table_name}: " "no FK-derived primary key attributes found." + f"Cannot create jobs table for {self._target.full_table_name}: no FK-derived primary key attributes found." ) pk_lines = "\n ".join(f"{name} : {dtype}" for name, dtype in pk_attrs) diff --git a/src/datajoint/objectref.py b/src/datajoint/objectref.py index 9a049b2cf..5d84fb96c 100644 --- a/src/datajoint/objectref.py +++ b/src/datajoint/objectref.py @@ -128,32 +128,6 @@ def to_json(self) -> dict: data["item_count"] = self.item_count return data - def to_dict(self) -> dict: - """ - Return the raw JSON metadata as a dictionary. - - This is useful for inspecting the stored metadata without triggering - any storage backend operations. The returned dict matches the JSON - structure stored in the database. - - Returns - ------- - dict - Dict containing the object metadata: - - - path: Relative storage path within the store - - url: Full URI (e.g., 's3://bucket/path') (optional) - - store: Store name (optional, None for default store) - - size: File/folder size in bytes (or None) - - hash: Content hash (or None) - - ext: File extension (or None) - - is_dir: True if folder - - timestamp: Upload timestamp - - mime_type: MIME type (files only, optional) - - item_count: Number of files (folders only, optional) - """ - return self.to_json() - def _ensure_backend(self): """Ensure storage backend is available for I/O operations.""" if self._backend is None: diff --git a/src/datajoint/settings.py b/src/datajoint/settings.py index 1c43b1ed2..5812f2257 100644 --- a/src/datajoint/settings.py +++ b/src/datajoint/settings.py @@ -389,7 +389,7 @@ def get_store_spec(self, store: str) -> dict[str, Any]: if protocol not in supported_protocols: raise DataJointError( f'Missing or invalid protocol in config.stores["{store}"]. ' - f'Supported protocols: {", ".join(supported_protocols)}' + f"Supported protocols: {', '.join(supported_protocols)}" ) # Define required and allowed keys by protocol @@ -479,7 +479,7 @@ def get_object_storage_spec(self) -> dict[str, Any]: supported_protocols = ("file", "s3", "gcs", "azure") if protocol not in supported_protocols: raise DataJointError( - f"Invalid object_storage.protocol: {protocol}. " f'Supported protocols: {", ".join(supported_protocols)}' + f"Invalid object_storage.protocol: {protocol}. Supported protocols: {', '.join(supported_protocols)}" ) # Build spec dict @@ -555,8 +555,7 @@ def get_object_store_spec(self, store_name: str | None = None) -> dict[str, Any] supported_protocols = ("file", "s3", "gcs", "azure") if protocol not in supported_protocols: raise DataJointError( - f"Invalid protocol for store '{store_name}': {protocol}. " - f'Supported protocols: {", ".join(supported_protocols)}' + f"Invalid protocol for store '{store_name}': {protocol}. Supported protocols: {', '.join(supported_protocols)}" ) # Use project_name from default config if not specified in store diff --git a/src/datajoint/storage.py b/src/datajoint/storage.py index 6dacbd7ec..846228137 100644 --- a/src/datajoint/storage.py +++ b/src/datajoint/storage.py @@ -24,13 +24,13 @@ # Characters safe for use in filenames and URLs TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" -# Supported remote URL protocols for copy insert -REMOTE_PROTOCOLS = ("s3://", "gs://", "gcs://", "az://", "abfs://", "http://", "https://") +# Supported URL protocols +URL_PROTOCOLS = ("file://", "s3://", "gs://", "gcs://", "az://", "abfs://", "http://", "https://") -def is_remote_url(path: str) -> bool: +def is_url(path: str) -> bool: """ - Check if a path is a remote URL. + Check if a path is a URL. Parameters ---------- @@ -40,21 +40,57 @@ def is_remote_url(path: str) -> bool: Returns ------- bool - True if path starts with a supported remote protocol. + True if path starts with a supported URL protocol. """ - if not isinstance(path, str): - return False - return path.lower().startswith(REMOTE_PROTOCOLS) + return path.lower().startswith(URL_PROTOCOLS) -def parse_remote_url(url: str) -> tuple[str, str]: +def normalize_to_url(path: str) -> str: """ - Parse a remote URL into protocol and path. + Normalize a path to URL form. + + Converts local filesystem paths to file:// URLs. URLs are returned unchanged. + + Parameters + ---------- + path : str + Path string (local path or URL). + + Returns + ------- + str + URL form of the path. + + Examples + -------- + >>> normalize_to_url("/data/file.dat") + 'file:///data/file.dat' + >>> normalize_to_url("s3://bucket/key") + 's3://bucket/key' + >>> normalize_to_url("file:///already/url") + 'file:///already/url' + """ + if is_url(path): + return path + # Convert local path to file:// URL + # Ensure absolute path and proper format + abs_path = str(Path(path).resolve()) + # Handle Windows paths (C:\...) vs Unix paths (/...) + if abs_path.startswith("/"): + return f"file://{abs_path}" + else: + # Windows: file:///C:/path + return f"file:///{abs_path.replace(chr(92), '/')}" + + +def parse_url(url: str) -> tuple[str, str]: + """ + Parse a URL into protocol and path. Parameters ---------- url : str - Remote URL (e.g., ``'s3://bucket/path/file.dat'``). + URL (e.g., ``'s3://bucket/path/file.dat'`` or ``'file:///path/to/file'``). Returns ------- @@ -65,11 +101,19 @@ def parse_remote_url(url: str) -> tuple[str, str]: ------ DataJointError If URL protocol is not supported. + + Examples + -------- + >>> parse_url("s3://bucket/key/file.dat") + ('s3', 'bucket/key/file.dat') + >>> parse_url("file:///data/file.dat") + ('file', '/data/file.dat') """ url_lower = url.lower() # Map URL schemes to fsspec protocols protocol_map = { + "file://": "file", "s3://": "s3", "gs://": "gcs", "gcs://": "gcs", @@ -84,7 +128,7 @@ def parse_remote_url(url: str) -> tuple[str, str]: path = url[len(prefix) :] return protocol, path - raise errors.DataJointError(f"Unsupported remote URL protocol: {url}") + raise errors.DataJointError(f"Unsupported URL protocol: {url}") def generate_token(length: int = 8) -> str: @@ -358,6 +402,53 @@ def _full_path(self, path: str | PurePosixPath) -> str: return str(Path(location) / path) return path + def get_url(self, path: str | PurePosixPath) -> str: + """ + Get the full URL for a path in storage. + + Returns a consistent URL representation for any storage backend, + including file:// URLs for local filesystem. + + Parameters + ---------- + path : str or PurePosixPath + Relative path within the storage location. + + Returns + ------- + str + Full URL (e.g., 's3://bucket/path' or 'file:///data/path'). + + Examples + -------- + >>> backend = StorageBackend({"protocol": "file", "location": "/data"}) + >>> backend.get_url("schema/table/file.dat") + 'file:///data/schema/table/file.dat' + + >>> backend = StorageBackend({"protocol": "s3", "bucket": "mybucket", ...}) + >>> backend.get_url("schema/table/file.dat") + 's3://mybucket/schema/table/file.dat' + """ + full_path = self._full_path(path) + + if self.protocol == "file": + # Ensure absolute path for file:// URL + abs_path = str(Path(full_path).resolve()) + if abs_path.startswith("/"): + return f"file://{abs_path}" + else: + # Windows path + return f"file:///{abs_path.replace(chr(92), '/')}" + elif self.protocol == "s3": + return f"s3://{full_path}" + elif self.protocol == "gcs": + return f"gs://{full_path}" + elif self.protocol == "azure": + return f"az://{full_path}" + else: + # Fallback: use protocol prefix + return f"{self.protocol}://{full_path}" + def put_file(self, local_path: str | Path, remote_path: str | PurePosixPath, metadata: dict | None = None) -> None: """ Upload a file from local filesystem to storage. @@ -674,7 +765,7 @@ def copy_from_url(self, source_url: str, dest_path: str | PurePosixPath) -> int: int Size of copied file in bytes. """ - protocol, source_path = parse_remote_url(source_url) + protocol, source_path = parse_url(source_url) full_dest = self._full_path(dest_path) logger.debug(f"copy_from_url: {protocol}://{source_path} -> {self.protocol}:{full_dest}") @@ -774,8 +865,8 @@ def source_is_directory(self, source: str) -> bool: bool True if source is a directory. """ - if is_remote_url(source): - protocol, path = parse_remote_url(source) + if is_url(source): + protocol, path = parse_url(source) source_fs = fsspec.filesystem(protocol) return source_fs.isdir(path) else: @@ -795,8 +886,8 @@ def source_exists(self, source: str) -> bool: bool True if source exists. """ - if is_remote_url(source): - protocol, path = parse_remote_url(source) + if is_url(source): + protocol, path = parse_url(source) source_fs = fsspec.filesystem(protocol) return source_fs.exists(path) else: @@ -817,8 +908,8 @@ def get_source_size(self, source: str) -> int | None: Size in bytes, or None if directory or cannot determine. """ try: - if is_remote_url(source): - protocol, path = parse_remote_url(source) + if is_url(source): + protocol, path = parse_url(source) source_fs = fsspec.filesystem(protocol) if source_fs.isdir(path): return None diff --git a/src/datajoint/table.py b/src/datajoint/table.py index 77611cb59..0040943c5 100644 --- a/src/datajoint/table.py +++ b/src/datajoint/table.py @@ -963,8 +963,7 @@ def cascade(table): transaction = False else: raise DataJointError( - "Delete cannot use a transaction within an ongoing transaction. " - "Set transaction=False or prompt=False." + "Delete cannot use a transaction within an ongoing transaction. Set transaction=False or prompt=False." ) # Cascading delete diff --git a/src/datajoint/user_tables.py b/src/datajoint/user_tables.py index 535276bbd..942179685 100644 --- a/src/datajoint/user_tables.py +++ b/src/datajoint/user_tables.py @@ -252,9 +252,7 @@ def drop(self, part_integrity: str = "enforce"): if part_integrity == "ignore": super().drop() elif part_integrity == "enforce": - raise DataJointError( - "Cannot drop a Part directly. Drop master instead, " "or use part_integrity='ignore' to force." - ) + raise DataJointError("Cannot drop a Part directly. Drop master instead, or use part_integrity='ignore' to force.") else: raise ValueError(f"part_integrity for drop must be 'enforce' or 'ignore', got {part_integrity!r}") diff --git a/tests/integration/test_object.py b/tests/integration/test_object.py index 8f44068e1..d4d42a461 100644 --- a/tests/integration/test_object.py +++ b/tests/integration/test_object.py @@ -759,94 +759,3 @@ def test_staged_insert_missing_pk_raises(self, schema_obj, mock_object_storage): with table.staged_insert1 as staged: # Don't set primary key staged.store("data_file", ".dat") - - -class TestRemoteURLSupport: - """Tests for remote URL detection and parsing.""" - - def test_is_remote_url_s3(self): - """Test S3 URL detection.""" - from datajoint.storage import is_remote_url - - assert is_remote_url("s3://bucket/path/file.dat") is True - assert is_remote_url("S3://bucket/path/file.dat") is True - - def test_is_remote_url_gcs(self): - """Test GCS URL detection.""" - from datajoint.storage import is_remote_url - - assert is_remote_url("gs://bucket/path/file.dat") is True - assert is_remote_url("gcs://bucket/path/file.dat") is True - - def test_is_remote_url_azure(self): - """Test Azure URL detection.""" - from datajoint.storage import is_remote_url - - assert is_remote_url("az://container/path/file.dat") is True - assert is_remote_url("abfs://container/path/file.dat") is True - - def test_is_remote_url_http(self): - """Test HTTP/HTTPS URL detection.""" - from datajoint.storage import is_remote_url - - assert is_remote_url("http://example.com/path/file.dat") is True - assert is_remote_url("https://example.com/path/file.dat") is True - - def test_is_remote_url_local_path(self): - """Test local paths are not detected as remote.""" - from datajoint.storage import is_remote_url - - assert is_remote_url("/local/path/file.dat") is False - assert is_remote_url("relative/path/file.dat") is False - assert is_remote_url("C:\\Windows\\path\\file.dat") is False - - def test_is_remote_url_non_string(self): - """Test non-string inputs return False.""" - from datajoint.storage import is_remote_url - - assert is_remote_url(None) is False - assert is_remote_url(123) is False - assert is_remote_url(Path("/local/path")) is False - - def test_parse_remote_url_s3(self): - """Test S3 URL parsing.""" - from datajoint.storage import parse_remote_url - - protocol, path = parse_remote_url("s3://bucket/path/file.dat") - assert protocol == "s3" - assert path == "bucket/path/file.dat" - - def test_parse_remote_url_gcs(self): - """Test GCS URL parsing.""" - from datajoint.storage import parse_remote_url - - protocol, path = parse_remote_url("gs://bucket/path/file.dat") - assert protocol == "gcs" - assert path == "bucket/path/file.dat" - - protocol, path = parse_remote_url("gcs://bucket/path/file.dat") - assert protocol == "gcs" - assert path == "bucket/path/file.dat" - - def test_parse_remote_url_azure(self): - """Test Azure URL parsing.""" - from datajoint.storage import parse_remote_url - - protocol, path = parse_remote_url("az://container/path/file.dat") - assert protocol == "abfs" - assert path == "container/path/file.dat" - - def test_parse_remote_url_http(self): - """Test HTTP URL parsing.""" - from datajoint.storage import parse_remote_url - - protocol, path = parse_remote_url("https://example.com/path/file.dat") - assert protocol == "https" - assert path == "example.com/path/file.dat" - - def test_parse_remote_url_unsupported(self): - """Test unsupported protocol raises error.""" - from datajoint.storage import parse_remote_url - - with pytest.raises(dj.DataJointError, match="Unsupported remote URL"): - parse_remote_url("ftp://server/path/file.dat") diff --git a/tests/unit/test_storage_urls.py b/tests/unit/test_storage_urls.py new file mode 100644 index 000000000..649d695b2 --- /dev/null +++ b/tests/unit/test_storage_urls.py @@ -0,0 +1,121 @@ +"""Unit tests for storage URL functions.""" + +import pytest + +from datajoint.storage import ( + URL_PROTOCOLS, + is_url, + normalize_to_url, + parse_url, +) + + +class TestURLProtocols: + """Test URL protocol constants.""" + + def test_url_protocols_includes_file(self): + """URL_PROTOCOLS should include file://.""" + assert "file://" in URL_PROTOCOLS + + def test_url_protocols_includes_s3(self): + """URL_PROTOCOLS should include s3://.""" + assert "s3://" in URL_PROTOCOLS + + def test_url_protocols_includes_cloud_providers(self): + """URL_PROTOCOLS should include major cloud providers.""" + assert "gs://" in URL_PROTOCOLS + assert "az://" in URL_PROTOCOLS + + +class TestIsUrl: + """Test is_url function.""" + + def test_s3_url(self): + assert is_url("s3://bucket/key") + + def test_gs_url(self): + assert is_url("gs://bucket/key") + + def test_file_url(self): + assert is_url("file:///path/to/file") + + def test_http_url(self): + assert is_url("http://example.com/file") + + def test_https_url(self): + assert is_url("https://example.com/file") + + def test_local_path_not_url(self): + assert not is_url("/path/to/file") + + def test_relative_path_not_url(self): + assert not is_url("relative/path/file.dat") + + def test_case_insensitive(self): + assert is_url("S3://bucket/key") + assert is_url("FILE:///path") + + +class TestNormalizeToUrl: + """Test normalize_to_url function.""" + + def test_local_path_to_file_url(self): + url = normalize_to_url("/data/file.dat") + assert url.startswith("file://") + assert "data/file.dat" in url + + def test_s3_url_unchanged(self): + url = "s3://bucket/key/file.dat" + assert normalize_to_url(url) == url + + def test_file_url_unchanged(self): + url = "file:///data/file.dat" + assert normalize_to_url(url) == url + + def test_relative_path_becomes_absolute(self): + url = normalize_to_url("relative/path.dat") + assert url.startswith("file://") + # Should be absolute (contain full path) + assert "/" in url[7:] # After "file://" + + +class TestParseUrl: + """Test parse_url function.""" + + def test_parse_s3(self): + protocol, path = parse_url("s3://bucket/key/file.dat") + assert protocol == "s3" + assert path == "bucket/key/file.dat" + + def test_parse_gs(self): + protocol, path = parse_url("gs://bucket/key") + assert protocol == "gcs" + assert path == "bucket/key" + + def test_parse_gcs(self): + protocol, path = parse_url("gcs://bucket/key") + assert protocol == "gcs" + assert path == "bucket/key" + + def test_parse_file(self): + protocol, path = parse_url("file:///data/file.dat") + assert protocol == "file" + assert path == "/data/file.dat" + + def test_parse_http(self): + protocol, path = parse_url("http://example.com/file") + assert protocol == "http" + assert path == "example.com/file" + + def test_parse_https(self): + protocol, path = parse_url("https://example.com/file") + assert protocol == "https" + assert path == "example.com/file" + + def test_unsupported_protocol_raises(self): + with pytest.raises(Exception, match="Unsupported URL protocol"): + parse_url("ftp://example.com/file") + + def test_local_path_raises(self): + with pytest.raises(Exception, match="Unsupported URL protocol"): + parse_url("/local/path") From c93f1c42a25b00ee62785bb6a542b6b95e9a7b4e Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 17:29:56 -0600 Subject: [PATCH 208/219] ci: Add pre/v2.0 branch to breaking and bug label patterns Ensures PR #1311 automatically receives breaking and bug labels. Co-Authored-By: Claude Opus 4.5 --- .github/pr_labeler.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pr_labeler.yaml b/.github/pr_labeler.yaml index ab722839f..51ce9afee 100644 --- a/.github/pr_labeler.yaml +++ b/.github/pr_labeler.yaml @@ -1,8 +1,8 @@ # https://github.com/actions/labeler breaking: -- head-branch: ['breaking', 'BREAKING'] +- head-branch: ['breaking', 'BREAKING', 'pre/v2.0'] bug: -- head-branch: ['fix', 'FIX', 'bug', 'BUG'] +- head-branch: ['fix', 'FIX', 'bug', 'BUG', 'pre/v2.0'] feature: - head-branch: ['feat', 'FEAT'] documentation: From 23967f4da0c0451294666d6c73d65759398f8e41 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 18:28:28 -0600 Subject: [PATCH 209/219] style: Reduce query result table font size to 75% Makes tables more compact in notebook displays. Co-Authored-By: Claude Opus 4.5 --- .secrets/database.password | 1 + .secrets/database.user | 1 + datajoint.json | 8 ++++++++ src/datajoint/preview.py | 6 +++--- 4 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .secrets/database.password create mode 100644 .secrets/database.user create mode 100644 datajoint.json diff --git a/.secrets/database.password b/.secrets/database.password new file mode 100644 index 000000000..150b5a035 --- /dev/null +++ b/.secrets/database.password @@ -0,0 +1 @@ +tutorial \ No newline at end of file diff --git a/.secrets/database.user b/.secrets/database.user new file mode 100644 index 000000000..93ca1422a --- /dev/null +++ b/.secrets/database.user @@ -0,0 +1 @@ +root \ No newline at end of file diff --git a/datajoint.json b/datajoint.json new file mode 100644 index 000000000..ce7282965 --- /dev/null +++ b/datajoint.json @@ -0,0 +1,8 @@ +{ + "database": { + "host": "127.0.0.1", + "port": 3306 + }, + "safemode": false, + "loglevel": "WARNING" +} diff --git a/src/datajoint/preview.py b/src/datajoint/preview.py index ddff041f2..5e2c92e5f 100644 --- a/src/datajoint/preview.py +++ b/src/datajoint/preview.py @@ -121,11 +121,11 @@ def get_html_display_value(tup, name, idx): border-collapse:collapse; } .Table th{ - background: #A0A0A0; color: #ffffff; padding:4px; border:#f0e0e0 1px solid; - font-weight: normal; font-family: monospace; font-size: 100%; + background: #A0A0A0; color: #ffffff; padding:2px 4px; border:#f0e0e0 1px solid; + font-weight: normal; font-family: monospace; font-size: 75%; } .Table td{ - padding:4px; border:#f0e0e0 1px solid; font-size:100%; + padding:2px 4px; border:#f0e0e0 1px solid; font-size: 75%; } .Table tr:nth-child(odd){ background: #ffffff; From 90e5c173468579f7a399068415b7f3cd1b84eb58 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 18:28:48 -0600 Subject: [PATCH 210/219] chore: Remove accidentally committed config files Co-Authored-By: Claude Opus 4.5 --- .secrets/database.password | 1 - .secrets/database.user | 1 - datajoint.json | 8 -------- 3 files changed, 10 deletions(-) delete mode 100644 .secrets/database.password delete mode 100644 .secrets/database.user delete mode 100644 datajoint.json diff --git a/.secrets/database.password b/.secrets/database.password deleted file mode 100644 index 150b5a035..000000000 --- a/.secrets/database.password +++ /dev/null @@ -1 +0,0 @@ -tutorial \ No newline at end of file diff --git a/.secrets/database.user b/.secrets/database.user deleted file mode 100644 index 93ca1422a..000000000 --- a/.secrets/database.user +++ /dev/null @@ -1 +0,0 @@ -root \ No newline at end of file diff --git a/datajoint.json b/datajoint.json deleted file mode 100644 index ce7282965..000000000 --- a/datajoint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "database": { - "host": "127.0.0.1", - "port": 3306 - }, - "safemode": false, - "loglevel": "WARNING" -} From 5db33592ddbdfc3949582ca016a80468be0ed8b1 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 18:48:35 -0600 Subject: [PATCH 211/219] docs: Use uint16 instead of native int in codec examples Co-Authored-By: Claude Opus 4.5 --- .secrets/database.password | 1 + .secrets/database.user | 1 + datajoint.json | 8 ++++++++ src/datajoint/codecs.py | 4 ++-- 4 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .secrets/database.password create mode 100644 .secrets/database.user create mode 100644 datajoint.json diff --git a/.secrets/database.password b/.secrets/database.password new file mode 100644 index 000000000..150b5a035 --- /dev/null +++ b/.secrets/database.password @@ -0,0 +1 @@ +tutorial \ No newline at end of file diff --git a/.secrets/database.user b/.secrets/database.user new file mode 100644 index 000000000..93ca1422a --- /dev/null +++ b/.secrets/database.user @@ -0,0 +1 @@ +root \ No newline at end of file diff --git a/datajoint.json b/datajoint.json new file mode 100644 index 000000000..ce7282965 --- /dev/null +++ b/datajoint.json @@ -0,0 +1,8 @@ +{ + "database": { + "host": "127.0.0.1", + "port": 3306 + }, + "safemode": false, + "loglevel": "WARNING" +} diff --git a/src/datajoint/codecs.py b/src/datajoint/codecs.py index 4ddb33d9c..211308d1c 100644 --- a/src/datajoint/codecs.py +++ b/src/datajoint/codecs.py @@ -27,7 +27,7 @@ def decode(self, stored, *, key=None): # Then use in table definitions: class MyTable(dj.Manual): definition = ''' - id : int + id : uint16 --- data : ''' @@ -81,7 +81,7 @@ class Codec(ABC): class Connectivity(dj.Manual): definition = ''' - id : int + id : uint16 --- graph_data : ''' From 7269d448fcfcba06f4983967e9e15307dd611056 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 18:49:10 -0600 Subject: [PATCH 212/219] chore: Remove accidentally committed config files Co-Authored-By: Claude Opus 4.5 --- .secrets/database.password | 1 - .secrets/database.user | 1 - datajoint.json | 8 -------- 3 files changed, 10 deletions(-) delete mode 100644 .secrets/database.password delete mode 100644 .secrets/database.user delete mode 100644 datajoint.json diff --git a/.secrets/database.password b/.secrets/database.password deleted file mode 100644 index 150b5a035..000000000 --- a/.secrets/database.password +++ /dev/null @@ -1 +0,0 @@ -tutorial \ No newline at end of file diff --git a/.secrets/database.user b/.secrets/database.user deleted file mode 100644 index 93ca1422a..000000000 --- a/.secrets/database.user +++ /dev/null @@ -1 +0,0 @@ -root \ No newline at end of file diff --git a/datajoint.json b/datajoint.json deleted file mode 100644 index ce7282965..000000000 --- a/datajoint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "database": { - "host": "127.0.0.1", - "port": 3306 - }, - "safemode": false, - "loglevel": "WARNING" -} From 8456f391864dd976752ddcfe93a6d04acfb1eff6 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Fri, 9 Jan 2026 18:49:25 -0600 Subject: [PATCH 213/219] chore: Add .secrets and datajoint.json to gitignore Co-Authored-By: Claude Opus 4.5 --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 93fc9385d..3c88c420c 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,7 @@ dj_local_conf.json # pixi environments .pixi _content/ + +# Local config +.secrets/ +datajoint.json From 82a9446923b05b2328329754c63a288c27dc9c3a Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 11 Jan 2026 07:33:30 -0600 Subject: [PATCH 214/219] refactor: Remove legacy docs/, add ARCHITECTURE.md Documentation is now consolidated in datajoint-docs repository. Changes: - Delete docs/ folder (legacy MkDocs infrastructure) - Create ARCHITECTURE.md with transpiler design docs - Update README.md links to point to docs.datajoint.com The Developer Guide remains in README.md. Internal architecture documentation for contributors is now in ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.5 --- ARCHITECTURE.md | 160 +++++++++++++++++ README.md | 11 +- docs/.markdownlint.yaml | 25 --- docs/Dockerfile | 16 -- docs/README.md | 96 ---------- docs/docker-compose.yaml | 40 ----- docs/mkdocs.yaml | 102 ----------- docs/pip_requirements.txt | 11 -- .../.overrides/.icons/main/company-logo.svg | 11 -- .../assets/images/company-logo-blue.png | Bin 41770 -> 0 bytes .../.overrides/assets/stylesheets/extra.css | 105 ----------- docs/src/.overrides/partials/nav.html | 53 ------ docs/src/api/make_pages.py | 17 -- docs/src/architecture/index.md | 34 ---- docs/src/architecture/transpilation.md | 170 ------------------ docs/src/develop.md | 101 ----------- docs/src/index.md | 44 ----- 17 files changed, 163 insertions(+), 833 deletions(-) create mode 100644 ARCHITECTURE.md delete mode 100644 docs/.markdownlint.yaml delete mode 100644 docs/Dockerfile delete mode 100644 docs/README.md delete mode 100644 docs/docker-compose.yaml delete mode 100644 docs/mkdocs.yaml delete mode 100644 docs/pip_requirements.txt delete mode 100644 docs/src/.overrides/.icons/main/company-logo.svg delete mode 100644 docs/src/.overrides/assets/images/company-logo-blue.png delete mode 100644 docs/src/.overrides/assets/stylesheets/extra.css delete mode 100644 docs/src/.overrides/partials/nav.html delete mode 100644 docs/src/api/make_pages.py delete mode 100644 docs/src/architecture/index.md delete mode 100644 docs/src/architecture/transpilation.md delete mode 100644 docs/src/develop.md delete mode 100644 docs/src/index.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 000000000..844f7ca05 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,160 @@ +# DataJoint Architecture + +Internal design documentation for DataJoint developers. + +## Design Principles + +DataJoint's architecture follows several key principles: + +1. **Immutable Query Expressions** — Query expressions are immutable; operators create new objects +2. **Lazy Evaluation** — Queries are not executed until data is fetched +3. **Query Optimization** — Unnecessary attributes are projected out before execution +4. **Semantic Matching** — Joins use lineage-based attribute matching + +## Module Overview + +| Module | Purpose | +|--------|---------| +| `expression.py` | QueryExpression base class and operators | +| `table.py` | Table class with data manipulation | +| `fetch.py` | Data retrieval implementation | +| `declare.py` | Table definition parsing | +| `heading.py` | Attribute and heading management | +| `blob.py` | Blob serialization | +| `codecs.py` | Type codec system | +| `connection.py` | Database connection management | +| `schemas.py` | Schema binding and activation | + +--- + +## Query System: SQL Transpilation + +This section describes how DataJoint translates query expressions to SQL. + +### MySQL Clause Evaluation Order + +MySQL differs from standard SQL in the sequence of evaluating SELECT statement clauses: + +``` +Standard SQL: FROM > WHERE > GROUP BY > HAVING > SELECT +MySQL: FROM > WHERE > SELECT > GROUP BY > HAVING +``` + +Moving `SELECT` to an earlier phase allows the `GROUP BY` and `HAVING` clauses to use +alias column names created by the `SELECT` clause. The current implementation targets +MySQL where table column aliases can be used in `HAVING`. + +### QueryExpression + +`QueryExpression` is the main object representing a distinct `SELECT` statement. +It implements operators `&`, `*`, and `proj` — restriction, join, and projection. + +- Property `heading` describes all attributes +- Operator `proj` creates a new heading +- Property `restriction` contains the `AndList` of conditions +- Operator `&` creates a new restriction appending the new condition +- Property `support` represents the `FROM` clause (list of QueryExpression objects or table names) +- The join operator `*` adds new elements to `support` + +From the user's perspective, `QueryExpression` objects are **immutable**: once created they +cannot be modified. All operators derive new objects. + +### Subqueries + +Projections, restrictions, and joins do not necessarily trigger new subqueries: the +resulting `QueryExpression` object simply merges the properties of its inputs into +self: `heading`, `restriction`, and `support`. + +The input object is treated as a subquery in the following cases: + +1. A restriction is applied that uses alias attributes in the heading +2. A projection uses an alias attribute to create a new alias attribute +3. A join is performed on an alias attribute +4. An Aggregation is used as a restriction + +Errors arise if: + +1. A restriction or projection attempts to use attributes not in the current heading +2. Attempting to join on attributes that are not join-compatible +3. Attempting to restrict by a non-join-compatible expression + +### Join Compatibility + +The join is always natural (i.e., *equijoin* on namesake attributes). + +**Version 0.13+:** Two query expressions are considered join-compatible if their namesake +attributes are either in the primary key or in a foreign key in both input expressions. + +**Future versions:** Compatibility will be further restricted to require that namesake +attributes ultimately derive from the same primary key attribute by being passed down +through foreign keys. + +The same join compatibility rules apply when restricting one query expression with another. + +### Join Mechanics + +Any restriction applied to the inputs of a join can be applied to its output. +Therefore, inputs that are not turned into subqueries donate their supports, +restrictions, and projections to the join itself. + +### Table + +`Table` is a subclass of `QueryExpression` implementing table manipulation methods: +`insert`, `insert1`, `delete`, `update1`, and `drop`. + +The restriction operator `&` applied to a `Table` preserves its class identity so that +the result remains of type `Table`. However, `proj` converts the result into a +`QueryExpression` object. + +### Aggregation + +`Aggregation` is a subclass of `QueryExpression`. Its main input is the *aggregating* +query expression and it takes an additional second input — the *aggregated* query expression. + +The SQL equivalent of aggregation is: + +1. The `NATURAL LEFT JOIN` of the two inputs +2. Followed by a `GROUP BY` on the primary key arguments of the first input +3. Followed by a projection + +The projection allows only calculated attributes using aggregating functions +(`SUM`, `AVG`, `COUNT`) applied to the aggregated (second) input's attributes. + +`Aggregation` supports all the same operators as `QueryExpression` except: + +1. `restriction` turns into a `HAVING` clause instead of `WHERE` +2. In joins, aggregation always turns into a subquery + +### Union + +`Union` is a subclass of `QueryExpression` resulting from the `+` operator on two +`QueryExpression` objects. Its `support` property contains the list of expressions +to unify (at least two). + +The `Union` operator performs an `OUTER JOIN` of its inputs provided that the inputs +have the same primary key and no secondary attributes in common. + +Union treats all its inputs as subqueries except for unrestricted Union objects. + +### Universal Sets (`dj.U`) + +`dj.U` is a special operand in query expressions that allows performing special +operations. By itself, it can never form a query and is not a subclass of +`QueryExpression`. Other query expressions are modified through participation in +operations with `dj.U`. + +### Query Backprojection + +Once a QueryExpression is used in a `fetch` operation or becomes a subquery in another +query, it can project out all unnecessary attributes from its own inputs, recursively. +This is implemented by the `finalize` method. + +This simplification produces much leaner queries resulting in improved query +performance, especially on complex queries with blob data, compensating for MySQL's +deficiencies in query optimization. + +--- + +## Contributing + +See the [Developer Guide](README.md#developer-guide) in README.md for development setup instructions. diff --git a/README.md b/README.md index 40a1a2c7c..83e5b1c9f 100644 --- a/README.md +++ b/README.md @@ -84,16 +84,11 @@ Scientific data includes both structured metadata and large data objects (time s pip install datajoint ``` -- [Documentation & Tutorials](https://docs.datajoint.com/core/datajoint-python/) +- [Documentation & Tutorials](https://docs.datajoint.com) -- [Interactive Tutorials](https://github.com/datajoint/datajoint-tutorials) on GitHub Codespaces +- [DataJoint Elements](https://datajoint.com/docs/elements/) — Catalog of example pipelines for neuroscience experiments -- [DataJoint Elements](https://docs.datajoint.com/elements/) - Catalog of example pipelines for neuroscience experiments - -- Contribute - - [Contribution Guidelines](https://docs.datajoint.com/about/contribute/) - - - [Developer Guide](https://docs.datajoint.com/core/datajoint-python/latest/develop/) +- [Architecture](ARCHITECTURE.md) — Internal design documentation for contributors ## Developer Guide diff --git a/docs/.markdownlint.yaml b/docs/.markdownlint.yaml deleted file mode 100644 index 7229b06e8..000000000 --- a/docs/.markdownlint.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# https://github.com/DavidAnson/markdownlint -# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md -MD007: false # Unordered list indentation -MD009: false # permit trailing spaces -MD013: - # previously we defined line_length to 88 which is better for python - # but not for markdown - line_length: - - strict: false - tables: false # disable for tables - headings: false # disable for headings -MD029: false # Ordered list item prefix -MD030: false # Number of spaces after a list -MD032: false # Lists should be surrounded by blank lines -MD033: # HTML elements allowed - allowed_elements: - - "div" - - "span" - - "a" - - "br" - - "sup" - - "figure" -MD034: false # Bare URLs OK -MD031: false # Spacing w/code blocks. Conflicts with `??? Note` and code tab styling -MD046: false # Spacing w/code blocks. Conflicts with `??? Note` and code tab styling diff --git a/docs/Dockerfile b/docs/Dockerfile deleted file mode 100644 index 10b1a9a05..000000000 --- a/docs/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:3 - -WORKDIR /main -COPY ./docs/pip_requirements.txt /main/docs/pip_requirements.txt -COPY ./datajoint /main/datajoint/ -COPY ./pyproject.toml /main/pyproject.toml - -RUN \ - # Install docs dependencies - pip install --no-cache-dir -r /main/docs/pip_requirements.txt && \ - # Install datajoint - pip install --no-cache-dir -e /main/ - -# Install dependencies first and use docker cache -# modify docs content won't cause image rebuild -COPY ./docs/ /main/docs/ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 4aecf0a69..000000000 --- a/docs/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Contribute to DataJoint Documentation - -This is the home for DataJoint software documentation as hosted at https://docs.datajoint.com/core/datajoint-python/latest/. - -## VSCode Linter Extensions and Settings - -The following extensions were used in developing these docs, with the corresponding -settings files: - -- Recommended extensions are already specified in `.vscode/extensions.json`, it will ask you to install them when you open the project if you haven't installed them. -- settings in `.vscode/settings.json` -- [MarkdownLinter](https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint): - - `.markdownlint.yaml` establishes settings for various - [linter rules](https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md) - - `.vscode/settings.json` formatting on save to fix linting - -- [CSpell](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker): `cspell.json` -has various ignored words. - -- [ReWrap](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap): `.vscode/settings.json` allows toggling -automated hard wrapping for files at 88 characters. This can also be keymapped to be -performed on individual paragraphs, see documentation. - -## With Virtual Environment - -conda -```bash -conda create -n djdocs -y -conda activate djdocs -``` -venv -```bash -python -m venv .venv -source .venv/bin/activate -``` - -Then install the required packages: -```bash -# go to the repo's root directory to generate API docs -# cd ~/datajoint-python/ - -# install mkdocs related requirements -pip install -r ./docs/pip_requirements.txt -# install datajoint, since API docs are generated from the package -pip install -e .[dev] -``` - -Run mkdocs at: http://127.0.0.1:8000/ -```bash -# go to the repo's root directory to generate API docs -# cd ~/datajoint-python/ - -# It will automatically reload the docs when changes are made -mkdocs serve --config-file ./docs/mkdocs.yaml -``` - -## With Docker - -> We mostly use Docker to simplify docs deployment - -Ensure you have `Docker` and `Docker Compose` installed. - -Then run the following: -```bash -# It will automatically reload the docs when changes are made -MODE="LIVE" docker compose up --build -``` - -Navigate to http://127.0.0.1:8000/ to preview the changes. - -This setup supports live-reloading so all that is needed is to save the markdown files -and/or `mkdocs.yaml` file to trigger a reload. - -## Mkdocs Warning Explanation - -> TL;DR: We need to do it this way for hosting, please keep it as is. - -```log -INFO - Doc file 'index.md' contains an unrecognized relative link './develop', it was left as is. Did you mean - 'develop.md'? -``` - -- We use reverse proxy to proxy our docs sites, here is the proxy flow: - - You hit `datajoint.com/*` on your browser - - It'll bring you to the reverse proxy server first, that you wouldn't notice - - when your URL ends with: - - `/` is the company page - - `/docs/` is the landing/navigation page hosted by datajoint/datajoint-docs's github pages - - `/docs/core/datajoint-python/` is the actual docs site hosted by datajoint/datajoint-python's github pages - - `/docs/elements/element-*/` is the actual docs site hosted by each element's github pages - -```log -WARNING - Doc file 'query/operators.md' contains a link '../../../images/concepts-operators-restriction.png', but - the target '../../images/concepts-operators-restriction.png' is not found among documentation files. -``` -- We use Github Pages to host our docs, the image references needs to follow the mkdocs's build directory structure, under `site/` directory once you run mkdocs. diff --git a/docs/docker-compose.yaml b/docs/docker-compose.yaml deleted file mode 100644 index 6a2eebb49..000000000 --- a/docs/docker-compose.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# MODE="LIVE|QA|BUILD" PACKAGE=datajoint UPSTREAM_REPO=https://github.com/datajoint/datajoint-python.git docker compose up --build -services: - docs: - build: - # some docs need to be dynamically generated from the datajoint PACKAGE - context: .. - dockerfile: docs/Dockerfile - image: datajoint-python-docs - environment: - MODE: ${MODE:-LIVE} # specify mode: LIVE, QA, BUILD - # specify package to generate API docs - PACKAGE: ${PACKAGE:-datajoint} - UPSTREAM_REPO: ${UPSTREAM_REPO:-https://github.com/datajoint/datajoint-python.git} - volumes: - - ..:/main - ports: - - 8000:8000 - command: - - bash - - -c - - | - set -e - if echo "$${MODE}" | grep -i live &>/dev/null; then - mkdocs serve --config-file /main/docs/mkdocs.yaml -a 0.0.0.0:8000 - elif echo "$${MODE}" | grep -iE "qa|build" &>/dev/null; then - git config --global --add safe.directory /main - git config --global user.name "GitHub Action" - git config --global user.email "action@github.com" - git config --global pull.rebase false - git branch -D gh-pages || true - git fetch $${UPSTREAM_REPO} gh-pages:gh-pages || true - mike deploy --ignore-remote-status --config-file /main/docs/mkdocs.yaml -u $$(grep -oP '\d+\.\d+' /main/$${PACKAGE}/version.py) latest - # mike set-default --config-file /main/docs/mkdocs.yaml latest - if echo "$${MODE}" | grep -i qa &>/dev/null; then - mike serve --config-file /main/docs/mkdocs.yaml -a 0.0.0.0:8000 - fi - else - echo "Unexpected mode..." - exit 1 - fi diff --git a/docs/mkdocs.yaml b/docs/mkdocs.yaml deleted file mode 100644 index db2ea16f9..000000000 --- a/docs/mkdocs.yaml +++ /dev/null @@ -1,102 +0,0 @@ -# ---------------------- PROJECT SPECIFIC --------------------------- - -site_name: DataJoint Python - Developer Documentation -site_description: Developer documentation for DataJoint Python contributors -repo_url: https://github.com/datajoint/datajoint-python -repo_name: datajoint/datajoint-python -nav: - - Home: index.md - - Contributing: develop.md - - Architecture: - - architecture/index.md - - SQL Transpilation: architecture/transpilation.md - - API Reference: api/ # defer to gen-files + literate-nav - -# ---------------------------- STANDARD ----------------------------- - -edit_uri: ./edit/master/docs/src -docs_dir: ./src -theme: - font: - text: Roboto Slab - code: Source Code Pro - name: material - custom_dir: src/.overrides - icon: - logo: main/company-logo - favicon: assets/images/company-logo-blue.png - features: - - toc.integrate - - content.code.annotate - palette: - - media: "(prefers-color-scheme: light)" - scheme: datajoint - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - toggle: - icon: material/brightness-4 - name: Switch to light mode -plugins: - - search - - autorefs - - mkdocstrings: - default_handler: python - handlers: - python: - paths: - - "../src" - options: - docstring_style: numpy - members_order: source - group_by_category: false - line_length: 88 - show_source: false - - gen-files: - scripts: - - ./src/api/make_pages.py - - literate-nav: - nav_file: navigation.md - - section-index -markdown_extensions: - - attr_list - - toc: - permalink: true - - pymdownx.emoji: - emoji_index: !!python/name:material.extensions.emoji.twemoji - emoji_generator: !!python/name:material.extensions.emoji.to_svg - options: - custom_icons: - - .overrides/.icons - - mdx_truly_sane_lists - - pymdownx.tabbed: - alternate_style: true - - admonition - - pymdownx.details - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.magiclink - - pymdownx.tasklist: - custom_checkbox: true - - md_in_html -extra: - generator: false - version: - provider: mike - social: - - icon: main/company-logo - link: https://www.datajoint.com - name: DataJoint - - icon: fontawesome/brands/github - link: https://github.com/datajoint - name: GitHub - - icon: fontawesome/brands/slack - link: https://datajoint.slack.com - name: Slack -extra_css: - - assets/stylesheets/extra.css diff --git a/docs/pip_requirements.txt b/docs/pip_requirements.txt deleted file mode 100644 index 057cf585d..000000000 --- a/docs/pip_requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -mkdocs-material -mkdocs-redirects -mkdocstrings -mkdocstrings-python -mike -mdx-truly-sane-lists -mkdocs-gen-files -mkdocs-literate-nav -mkdocs-exclude-search -mkdocs-jupyter -mkdocs-section-index diff --git a/docs/src/.overrides/.icons/main/company-logo.svg b/docs/src/.overrides/.icons/main/company-logo.svg deleted file mode 100644 index e876313cd..000000000 --- a/docs/src/.overrides/.icons/main/company-logo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - diff --git a/docs/src/.overrides/assets/images/company-logo-blue.png b/docs/src/.overrides/assets/images/company-logo-blue.png deleted file mode 100644 index d15194b8db09a9fabae8da2cdb2f2a4d3c820a96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41770 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 diff --git a/docs/src/.overrides/assets/stylesheets/extra.css b/docs/src/.overrides/assets/stylesheets/extra.css deleted file mode 100644 index ca0cca378..000000000 --- a/docs/src/.overrides/assets/stylesheets/extra.css +++ /dev/null @@ -1,105 +0,0 @@ -:root { - --dj-primary: #00a0df; - --dj-secondary: #ff5113; - --dj-background: #808285; - --dj-black: #000000; - --dj-white: #ffffff; -} - -/* footer previous/next navigation */ -.md-footer__inner:not([hidden]) { - display: none -} - -/* footer social icons */ -html a[title="DataJoint"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="Slack"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="LinkedIn"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="Twitter"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="GitHub"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="DockerHub"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="PyPI"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="StackOverflow"].md-social__link svg { - color: var(--dj-primary); -} - -html a[title="YouTube"].md-social__link svg { - color: var(--dj-primary); -} - -[data-md-color-scheme="datajoint"] { - /* ribbon */ - /* ribbon + markdown heading expansion */ - --md-primary-fg-color: var(--dj-black); - /* ribbon text */ - --md-primary-bg-color: var(--dj-primary); - - /* navigation */ - /* navigation header + links */ - --md-typeset-a-color: var(--dj-primary); - /* navigation on hover + diagram outline */ - --md-accent-fg-color: var(--dj-secondary); - - /* main */ - /* main header + already viewed*/ - --md-default-fg-color--light: var(--dj-background); - /* primary text */ - --md-typeset-color: var(--dj-black); - /* code comments + diagram text */ - --md-code-fg-color: var(--dj-primary); - - /* footer */ - /* previous/next text */ - /* --md-footer-fg-color: var(--dj-primary); */ -} - -[data-md-color-scheme="slate"] { - /* ribbon */ - /* ribbon + markdown heading expansion */ - --md-primary-fg-color: var(--dj-primary); - /* ribbon text */ - --md-primary-bg-color: var(--dj-white); - - /* navigation */ - /* navigation header + links */ - --md-typeset-a-color: var(--dj-primary); - /* navigation on hover + diagram outline */ - --md-accent-fg-color: var(--dj-secondary); - - /* main */ - /* main header + already viewed*/ - /* --md-default-fg-color--light: var(--dj-background); */ - /* primary text */ - --md-typeset-color: var(--dj-white); - /* code comments + diagram text */ - --md-code-fg-color: var(--dj-primary); - - /* footer */ - /* previous/next text */ - /* --md-footer-fg-color: var(--dj-white); */ -} - -[data-md-color-scheme="slate"] .jupyter-wrapper .Table Td { - color: var(--dj-black) -} diff --git a/docs/src/.overrides/partials/nav.html b/docs/src/.overrides/partials/nav.html deleted file mode 100644 index a0529199d..000000000 --- a/docs/src/.overrides/partials/nav.html +++ /dev/null @@ -1,53 +0,0 @@ - - -{% import "partials/nav-item.html" as item with context %} - - -{% set class = "md-nav md-nav--primary" %} -{% if "navigation.tabs" in features %} - {% set class = class ~ " md-nav--lifted" %} -{% endif %} -{% if "toc.integrate" in features %} - {% set class = class ~ " md-nav--integrated" %} -{% endif %} - - - \ No newline at end of file diff --git a/docs/src/api/make_pages.py b/docs/src/api/make_pages.py deleted file mode 100644 index 25dc29943..000000000 --- a/docs/src/api/make_pages.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Generate the api pages and navigation.""" - -import os -from pathlib import Path - -import mkdocs_gen_files - -package = os.getenv("PACKAGE", "datajoint") -nav = mkdocs_gen_files.Nav() -for path in sorted(Path(package).glob("**/*.py")): - with mkdocs_gen_files.open(f"api/{path.with_suffix('')}.md", "w") as f: - module_path = ".".join([p for p in path.with_suffix("").parts if p != "__init__"]) - print(f"::: {module_path}", file=f) - nav[path.parts] = f"{path.with_suffix('')}.md" - -with mkdocs_gen_files.open("api/navigation.md", "w") as nav_file: - nav_file.writelines(nav.build_literate_nav()) diff --git a/docs/src/architecture/index.md b/docs/src/architecture/index.md deleted file mode 100644 index 953fd7962..000000000 --- a/docs/src/architecture/index.md +++ /dev/null @@ -1,34 +0,0 @@ -# Architecture - -Internal design documentation for DataJoint developers. - -## Query System - -- [SQL Transpilation](transpilation.md) — How DataJoint translates query expressions to SQL - -## Design Principles - -DataJoint's architecture follows several key principles: - -1. **Immutable Query Expressions** — Query expressions are immutable; operators create new objects -2. **Lazy Evaluation** — Queries are not executed until data is fetched -3. **Query Optimization** — Unnecessary attributes are projected out before execution -4. **Semantic Matching** — Joins use lineage-based attribute matching - -## Module Overview - -| Module | Purpose | -|--------|---------| -| `expression.py` | QueryExpression base class and operators | -| `table.py` | Table class with data manipulation | -| `fetch.py` | Data retrieval implementation | -| `declare.py` | Table definition parsing | -| `heading.py` | Attribute and heading management | -| `blob.py` | Blob serialization | -| `codecs.py` | Type codec system | -| `connection.py` | Database connection management | -| `schemas.py` | Schema binding and activation | - -## Contributing - -See the [Contributing Guide](../develop.md) for development setup instructions. diff --git a/docs/src/architecture/transpilation.md b/docs/src/architecture/transpilation.md deleted file mode 100644 index b8d81d42a..000000000 --- a/docs/src/architecture/transpilation.md +++ /dev/null @@ -1,170 +0,0 @@ -# Transpiler Design - -This section contains the information and reasoning that went into the design of the -DataJoint-to-SQL transpiler. - -MySQL appears to differ from standard SQL by the sequence of evaluating the clauses of -the SELECT statement. - -``` -Standard SQL: FROM > WHERE > GROUP BY > HAVING > SELECT -MySQL: FROM > WHERE > SELECT > GROUP BY > HAVING -``` - - - -Moving `SELECT` to an earlier phase allows the `GROUP BY` and `HAVING` clauses to use -alias column names created by the `SELECT` clause. -The current implementation targets the MySQL implementation where table column aliases -can be used in `HAVING`. -If postgres or CockroachDB cannot be coerced to work this way, restrictions of -aggregations will have to be updated accordingly. - -## QueryExpression - -`QueryExpression` is the main object representing a distinct `SELECT` statement. -It implements operators `&`, `*`, and `proj` — restriction, join, and projection. - -Property `heading` describes all attributes. - -Operator `proj` creates a new heading. - -Property `restriction` contains the `AndList` of conditions. Operator `&` creates a new -restriction appending the new condition to the input's restriction. - -Property `support` represents the `FROM` clause and contains a list of either -`QueryExpression` objects or table names in the case of base queries. -The join operator `*` adds new elements to the `support` attribute. - -At least one element must be present in `support`. Multiple elements in `support` -indicate a join. - -From the user's perspective `QueryExpression` objects are immutable: once created they -cannot be modified. All operators derive new objects. - -### Alias attributes - -`proj` can create an alias attribute by renaming an existing attribute or calculating a -new attribute. -Alias attributes are the primary reason why subqueries are sometimes required. - -### Subqueries - -Projections, restrictions, and joins do not necessarily trigger new subqueries: the -resulting `QueryExpression` object simply merges the properties of its inputs into -self: `heading`, `restriction`, and `support`. - -The input object is treated as a subquery in the following cases: - -1. A restriction is applied that uses alias attributes in the heading. -2. A projection uses an alias attribute to create a new alias attribute. -3. A join is performed on an alias attribute. -4. An Aggregation is used a restriction. - -An error arises if - -1. If a restriction or a projection attempts to use attributes not in the current -heading. -2. If attempting to join on attributes that are not join-compatible -3. If attempting to restrict by a non-join-compatible expression - -A subquery is created by creating a new `QueryExpression` object (or a subclass object) -with its `support` pointing to the input object. - -### Join compatibility - -The join is always natural (i.e. *equijoin* on the namesake attributes). - -**Before version 0.13:** As of version `0.12.*` and earlier, two query expressions were -considered join-compatible if their namesake attributes were the primary key of at -least one of the input expressions. This rule was easiest to implement but does not -provide best semantics. - -**Version 0.13:** In version `0.13.*`, two query expressions are considered -join-compatible if their namesake attributes are either in the primary key or in a -foreign key in both input expressions. - -**Future (potentially version 0.14+):** -This compatibility requirement will be further restricted to require that the namesake -attributes ultimately derive from the same primary key attribute by being passed down -through foreign keys. - -The same join compatibility rules apply when restricting one query expression with -another. - -### Join mechanics - -Any restriction applied to the inputs of a join can be applied to its output. -Therefore, those inputs that are not turned into queries donate their supports, -restrictions, and projections to the join itself. - -## Table - -`Table` is a subclass of `QueryExpression` implementing table manipulation methods such -as `insert`, `insert1`, `delete`, `update1`, and `drop`. - -The restriction operator `&` applied to a `Table` preserves its class identity so that -the result remains of type `Table`. -However, `proj` converts the result into a `QueryExpression` object. This may produce a -base query that is not an instance of Table. - -## Aggregation - -`Aggregation` is a subclass of `QueryExpression`. -Its main input is the *aggregating* query expression and it takes an additional second -input — the *aggregated* query expression. - -The SQL equivalent of aggregation is - -1. the NATURAL LEFT JOIN of the two inputs. -2. followed by a GROUP BY on the primary key arguments of the first input -3. followed by a projection. - -The projection works the same as `.proj` with respect to the first input. -With respect to the second input, the projection part of aggregation allows only -calculated attributes that use aggregating functions (*eg* `SUM`, `AVG`, `COUNT`) -applied to the attributes of the aggregated (second) input and non-aggregating -functions on the attribute of the aggregating (first) input. - -`Aggregation` supports all the same operators as `QueryExpression` except: - -1. `restriction` turns into a `HAVING` clause instead of a `WHERE` clause. This allows -applying any valid restriction without making a subquery (at least for MySQL). -Therefore, restricting an `Aggregation` object never results in a subquery. -2. In joins, aggregation always turns into a subquery. - -All other rules for subqueries remain the same as for `QueryExpression` - -## Union - -`Union` is a subclass of `QueryExpression`. -A `Union` object results from the `+` operator on two `QueryExpression` objects. -Its `support` property contains the list of expressions (at least two) to unify. -Thus the `+` operator on unions simply merges their supports, making a bigger union. - -The `Union` operator performs an OUTER JOIN of its inputs provided that the inputs have -the same primary key and no secondary attributes in common. - -Union treats all its inputs as subqueries except for unrestricted Union objects. - -## Universal Sets `dj.U` - -`dj.U` is a special operand in query expressions that allows performing special -operations. By itself, it can never form a query and is not a subclass of -`QueryExpression`. Other query expressions are modified through participation in -operations with `dj.U`. - -### Aggregating by `dj.U` - -### Restricting a `dj.U` object with a `QueryExpression` object - -### Joining a `dj.U` object - -## Query "Backprojection" - -Once a QueryExpression is used in a `fetch` operation or becomes a subquery in another -query, it can project out all unnecessary attributes from its own inputs, recursively. -This is implemented by the `finalize` method. -This simplification produces much leaner queries resulting in improved query -performance in version 0.13, especially on complex queries with blob data, compensating -for MySQL's deficiencies in query optimization. diff --git a/docs/src/develop.md b/docs/src/develop.md deleted file mode 100644 index 4643683b6..000000000 --- a/docs/src/develop.md +++ /dev/null @@ -1,101 +0,0 @@ -# Contributing Guide - -## Quick Start - -```bash -# Clone the repository -git clone https://github.com/datajoint/datajoint-python.git -cd datajoint-python - -# Create virtual environment (Python 3.10+) -python -m venv .venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate - -# Install with development dependencies -pip install -e ".[dev]" - -# Install pre-commit hooks -pre-commit install - -# Run tests -pytest tests -``` - -## Development Environment - -### Local Setup - -Requirements: - -- Python 3.10 or higher -- MySQL 8.0+ or Docker (for running tests) - -The `[dev]` extras install all development tools: pytest, pre-commit, black, ruff, and documentation builders. - -### Using Docker for Database - -Tests require a MySQL database. Start one with Docker: - -```bash -docker compose up -d db -``` - -Configure connection (or set environment variables): - -```bash -export DJ_HOST=localhost -export DJ_USER=root -export DJ_PASS=password -``` - -### Alternative: GitHub Codespaces - -For a pre-configured environment, use [GitHub Codespaces](https://github.com/features/codespaces): - -1. Fork the repository -2. Click "Create codespace on master" -3. Wait for environment to build (~6 minutes first time, ~2 minutes from cache) - -## Code Quality - -### Pre-commit Hooks - -Pre-commit runs automatically on `git commit`. To run manually: - -```bash -pre-commit run --all-files -``` - -Hooks include: - -- **ruff** — Linting and import sorting -- **black** — Code formatting -- **mypy** — Type checking (optional) - -### Running Tests - -```bash -# Full test suite with coverage -pytest -sv --cov=datajoint tests - -# Single test file -pytest tests/test_connection.py - -# Single test function -pytest tests/test_connection.py::test_dj_conn -v -``` - -## Submitting Changes - -1. Create a feature branch from `master` -2. Make your changes -3. Ensure tests pass and pre-commit is clean -4. Submit a pull request - -PRs trigger CI checks automatically. All checks must pass before merge. - -## Documentation - -Docstrings use NumPy style. See [DOCSTRING_STYLE.md](https://github.com/datajoint/datajoint-python/blob/master/DOCSTRING_STYLE.md) for guidelines. - -User documentation is maintained at [docs.datajoint.com](https://docs.datajoint.com). diff --git a/docs/src/index.md b/docs/src/index.md deleted file mode 100644 index 63b318a1c..000000000 --- a/docs/src/index.md +++ /dev/null @@ -1,44 +0,0 @@ -# DataJoint for Python - -DataJoint is an open-source Python framework for building scientific data pipelines. -It implements the **Relational Workflow Model**—a paradigm that extends relational -databases with native support for computational workflows. - -## Documentation - -**User documentation** is available at **[docs.datajoint.com](https://docs.datajoint.com)**, including: - -- Tutorials and getting started guides -- Concepts and explanations -- How-to guides -- API reference - -## This Site - -This site contains **developer documentation** for contributors to the DataJoint codebase: - -- [Contributing Guide](develop.md) — Development environment setup -- [Architecture](architecture/index.md) — Internal design documentation -- [API Reference](api/) — Auto-generated from source - -## Quick Links - -| Resource | Link | -|----------|------| -| User Documentation | [docs.datajoint.com](https://docs.datajoint.com) | -| GitHub Repository | [github.com/datajoint/datajoint-python](https://github.com/datajoint/datajoint-python) | -| PyPI Package | [pypi.org/project/datajoint](https://pypi.org/project/datajoint) | -| Issue Tracker | [GitHub Issues](https://github.com/datajoint/datajoint-python/issues) | -| Community | [DataJoint Slack](https://datajoint.slack.com) | - -## Installation - -```bash -pip install datajoint -``` - -## License - -DataJoint is released under the [Apache 2.0 License](https://github.com/datajoint/datajoint-python/blob/master/LICENSE). - -Copyright 2024 DataJoint Inc. and contributors. From 5d2ba055b8df1e659496f386ed819762d2a4778f Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 11 Jan 2026 07:56:03 -0600 Subject: [PATCH 215/219] refactor: Remove ARCHITECTURE.md, content moved to docs spec Transpilation documentation moved to datajoint-docs query-algebra spec. Developer docs now consolidated in README.md. Co-Authored-By: Claude Opus 4.5 --- ARCHITECTURE.md | 160 ------------------------------------------------ 1 file changed, 160 deletions(-) delete mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 844f7ca05..000000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,160 +0,0 @@ -# DataJoint Architecture - -Internal design documentation for DataJoint developers. - -## Design Principles - -DataJoint's architecture follows several key principles: - -1. **Immutable Query Expressions** — Query expressions are immutable; operators create new objects -2. **Lazy Evaluation** — Queries are not executed until data is fetched -3. **Query Optimization** — Unnecessary attributes are projected out before execution -4. **Semantic Matching** — Joins use lineage-based attribute matching - -## Module Overview - -| Module | Purpose | -|--------|---------| -| `expression.py` | QueryExpression base class and operators | -| `table.py` | Table class with data manipulation | -| `fetch.py` | Data retrieval implementation | -| `declare.py` | Table definition parsing | -| `heading.py` | Attribute and heading management | -| `blob.py` | Blob serialization | -| `codecs.py` | Type codec system | -| `connection.py` | Database connection management | -| `schemas.py` | Schema binding and activation | - ---- - -## Query System: SQL Transpilation - -This section describes how DataJoint translates query expressions to SQL. - -### MySQL Clause Evaluation Order - -MySQL differs from standard SQL in the sequence of evaluating SELECT statement clauses: - -``` -Standard SQL: FROM > WHERE > GROUP BY > HAVING > SELECT -MySQL: FROM > WHERE > SELECT > GROUP BY > HAVING -``` - -Moving `SELECT` to an earlier phase allows the `GROUP BY` and `HAVING` clauses to use -alias column names created by the `SELECT` clause. The current implementation targets -MySQL where table column aliases can be used in `HAVING`. - -### QueryExpression - -`QueryExpression` is the main object representing a distinct `SELECT` statement. -It implements operators `&`, `*`, and `proj` — restriction, join, and projection. - -- Property `heading` describes all attributes -- Operator `proj` creates a new heading -- Property `restriction` contains the `AndList` of conditions -- Operator `&` creates a new restriction appending the new condition -- Property `support` represents the `FROM` clause (list of QueryExpression objects or table names) -- The join operator `*` adds new elements to `support` - -From the user's perspective, `QueryExpression` objects are **immutable**: once created they -cannot be modified. All operators derive new objects. - -### Subqueries - -Projections, restrictions, and joins do not necessarily trigger new subqueries: the -resulting `QueryExpression` object simply merges the properties of its inputs into -self: `heading`, `restriction`, and `support`. - -The input object is treated as a subquery in the following cases: - -1. A restriction is applied that uses alias attributes in the heading -2. A projection uses an alias attribute to create a new alias attribute -3. A join is performed on an alias attribute -4. An Aggregation is used as a restriction - -Errors arise if: - -1. A restriction or projection attempts to use attributes not in the current heading -2. Attempting to join on attributes that are not join-compatible -3. Attempting to restrict by a non-join-compatible expression - -### Join Compatibility - -The join is always natural (i.e., *equijoin* on namesake attributes). - -**Version 0.13+:** Two query expressions are considered join-compatible if their namesake -attributes are either in the primary key or in a foreign key in both input expressions. - -**Future versions:** Compatibility will be further restricted to require that namesake -attributes ultimately derive from the same primary key attribute by being passed down -through foreign keys. - -The same join compatibility rules apply when restricting one query expression with another. - -### Join Mechanics - -Any restriction applied to the inputs of a join can be applied to its output. -Therefore, inputs that are not turned into subqueries donate their supports, -restrictions, and projections to the join itself. - -### Table - -`Table` is a subclass of `QueryExpression` implementing table manipulation methods: -`insert`, `insert1`, `delete`, `update1`, and `drop`. - -The restriction operator `&` applied to a `Table` preserves its class identity so that -the result remains of type `Table`. However, `proj` converts the result into a -`QueryExpression` object. - -### Aggregation - -`Aggregation` is a subclass of `QueryExpression`. Its main input is the *aggregating* -query expression and it takes an additional second input — the *aggregated* query expression. - -The SQL equivalent of aggregation is: - -1. The `NATURAL LEFT JOIN` of the two inputs -2. Followed by a `GROUP BY` on the primary key arguments of the first input -3. Followed by a projection - -The projection allows only calculated attributes using aggregating functions -(`SUM`, `AVG`, `COUNT`) applied to the aggregated (second) input's attributes. - -`Aggregation` supports all the same operators as `QueryExpression` except: - -1. `restriction` turns into a `HAVING` clause instead of `WHERE` -2. In joins, aggregation always turns into a subquery - -### Union - -`Union` is a subclass of `QueryExpression` resulting from the `+` operator on two -`QueryExpression` objects. Its `support` property contains the list of expressions -to unify (at least two). - -The `Union` operator performs an `OUTER JOIN` of its inputs provided that the inputs -have the same primary key and no secondary attributes in common. - -Union treats all its inputs as subqueries except for unrestricted Union objects. - -### Universal Sets (`dj.U`) - -`dj.U` is a special operand in query expressions that allows performing special -operations. By itself, it can never form a query and is not a subclass of -`QueryExpression`. Other query expressions are modified through participation in -operations with `dj.U`. - -### Query Backprojection - -Once a QueryExpression is used in a `fetch` operation or becomes a subquery in another -query, it can project out all unnecessary attributes from its own inputs, recursively. -This is implemented by the `finalize` method. - -This simplification produces much leaner queries resulting in improved query -performance, especially on complex queries with blob data, compensating for MySQL's -deficiencies in query optimization. - ---- - -## Contributing - -See the [Developer Guide](README.md#developer-guide) in README.md for development setup instructions. From 3c9a9bf6af18c248d53c6ee5d24e57a1056fab98 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 11 Jan 2026 08:37:24 -0600 Subject: [PATCH 216/219] docs: Archive CHANGELOG.md, add release notes guidelines - Rename CHANGELOG.md to CHANGELOG-archive.md with redirect to GitHub Releases - Add "Writing Release Notes" section to RELEASE_MEMO.md: - Categories (BREAKING, Added, Changed, Deprecated, Fixed, Security) - Format template with examples - Guidelines for good release notes - PR label mapping for release drafter Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md => CHANGELOG-archive.md | 14 ++++-- RELEASE_MEMO.md | 65 +++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) rename CHANGELOG.md => CHANGELOG-archive.md (98%) diff --git a/CHANGELOG.md b/CHANGELOG-archive.md similarity index 98% rename from CHANGELOG.md rename to CHANGELOG-archive.md index 4bf094509..46241c669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG-archive.md @@ -1,7 +1,15 @@ -## Release notes +# Changelog Archive -**Note:** This file is no longer updated. See the GitHub change log page for the -latest release notes: . +> **This file is archived.** For current release notes, see: +> +> **[GitHub Releases](https://github.com/datajoint/datajoint-python/releases)** +> +> Release notes are now automatically generated from pull request labels and descriptions. +> This file preserves the history of releases through version 0.14.3. + +--- + +## Historical Release Notes ### 0.14.3 -- Sep 23, 2024 - Added - `dj.Top` restriction - PR [#1024](https://github.com/datajoint/datajoint-python/issues/1024)) PR [#1084](https://github.com/datajoint/datajoint-python/pull/1084) diff --git a/RELEASE_MEMO.md b/RELEASE_MEMO.md index 25fdc6ca0..c65d04420 100644 --- a/RELEASE_MEMO.md +++ b/RELEASE_MEMO.md @@ -1,4 +1,67 @@ -# DataJoint 2.0 Release Memo +# DataJoint Release Memo + +## Writing Release Notes + +Good release notes help users understand what changed and whether they need to take action. + +### Categories + +Organize changes into these categories (in order): + +| Category | When to Use | Example | +|----------|-------------|---------| +| **BREAKING** | Changes that require user action | API changes, removed features | +| **Added** | New features | New methods, new options | +| **Changed** | Behavior changes (non-breaking) | Performance improvements, defaults | +| **Deprecated** | Features marked for removal | Old syntax warnings | +| **Fixed** | Bug fixes | Error corrections | +| **Security** | Security patches | Vulnerability fixes | + +### Format + +```markdown +## What's Changed + +### BREAKING CHANGES +- **`fetch()` removed** — Use `to_dicts()`, `to_pandas()`, or `to_arrays()` instead (#123) + +### Added +- New `to_polars()` method for Polars DataFrame output (#456) +- Support for custom codecs via `@codec` decorator (#789) + +### Changed +- Improved query performance for complex joins (2-3x faster) +- Default connection timeout increased to 30s + +### Fixed +- Fixed incorrect NULL handling in aggregations (#234) + +### Full Changelog +https://github.com/datajoint/datajoint-python/compare/v0.14.3...v2.0.0 +``` + +### Guidelines + +1. **Lead with breaking changes** — Users need to see these first +2. **Explain the "why"** — Not just what changed, but why it matters +3. **Link to PRs/issues** — For users who want details +4. **Use imperative mood** — "Add feature" not "Added feature" +5. **Be concise** — One line per change, details in PR + +### PR Labels + +The release drafter uses PR labels to categorize changes: + +| Label | Category | +|-------|----------| +| `breaking` | BREAKING CHANGES | +| `enhancement` | Added | +| `bug` | Fixed | +| `documentation` | (usually excluded) | + +Ensure PRs have appropriate labels before merging. + +--- ## PyPI Release Process From 587ee69ce3b1629f4c0eb8c59621a65c51f89ed8 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 11 Jan 2026 08:42:09 -0600 Subject: [PATCH 217/219] docs: Streamline documentation, create CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Slim README.md to essentials (intro, badges, install, links) - Create CONTRIBUTING.md with: - Development setup (pixi and pip) - Test running instructions - Pre-commit hooks - Environment variables - Condensed docstring style guide - Delete DOCSTRING_STYLE.md (merged into CONTRIBUTING.md) README: 218 → 82 lines All detailed docs now at docs.datajoint.com Co-Authored-By: Claude Opus 4.5 --- CONTRIBUTING.md | 152 ++++++++++++++ DOCSTRING_STYLE.md | 499 --------------------------------------------- README.md | 190 +++-------------- 3 files changed, 179 insertions(+), 662 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 DOCSTRING_STYLE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..68bf24175 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,152 @@ +# Contributing to DataJoint + +## Development Setup + +### Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) (Docker daemon must be running) +- [pixi](https://pixi.sh) (recommended) or Python 3.10+ + +### Quick Start with pixi + +[pixi](https://pixi.sh) manages all dependencies including Python, graphviz, and test tools: + +```bash +git clone https://github.com/datajoint/datajoint-python.git +cd datajoint-python + +# Run tests (containers managed automatically) +pixi run test + +# Run with coverage +pixi run test-cov + +# Run pre-commit hooks +pixi run pre-commit run --all-files +``` + +### Alternative: Using pip + +```bash +pip install -e ".[test]" +pytest tests/ +``` + +--- + +## Running Tests + +Tests use [testcontainers](https://testcontainers.com/) to automatically manage MySQL and MinIO containers. No manual `docker-compose up` required. + +```bash +pixi run test # All tests +pixi run test-cov # With coverage +pixi run -e test pytest tests/unit/ # Unit tests only +pixi run -e test pytest tests/integration/test_blob.py -v # Specific file +``` + +**macOS Docker Desktop users:** If tests fail to connect: +```bash +export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock +``` + +### External Containers (for debugging) + +```bash +docker compose up -d db minio +DJ_USE_EXTERNAL_CONTAINERS=1 pixi run test +docker compose down +``` + +### Full Docker + +```bash +docker compose --profile test up djtest --build +``` + +--- + +## Pre-commit Hooks + +Hooks run automatically on `git commit`. All must pass. + +```bash +pixi run pre-commit install # First time only +pixi run pre-commit run --all-files # Run manually +``` + +Hooks include: **ruff** (lint/format), **codespell**, YAML/JSON/TOML validation. + +--- + +## Before Submitting a PR + +1. `pixi run test` — All tests pass +2. `pixi run pre-commit run --all-files` — Hooks pass +3. `pixi run test-cov` — Coverage maintained + +--- + +## Environment Variables + +For `DJ_USE_EXTERNAL_CONTAINERS=1`: + +| Variable | Default | Description | +|----------|---------|-------------| +| `DJ_HOST` | `localhost` | MySQL hostname | +| `DJ_PORT` | `3306` | MySQL port | +| `DJ_USER` | `root` | MySQL username | +| `DJ_PASS` | `password` | MySQL password | +| `S3_ENDPOINT` | `localhost:9000` | MinIO endpoint | + +--- + +## Docstring Style + +Use **NumPy-style** docstrings for all public APIs: + +```python +def insert(self, rows, *, replace=False): + """ + Insert rows into the table. + + Parameters + ---------- + rows : iterable + Rows to insert. Each row can be a dict, numpy record, or sequence. + replace : bool, optional + If True, replace existing rows with matching keys. Default is False. + + Returns + ------- + None + + Raises + ------ + DuplicateError + When inserting a duplicate key without ``replace=True``. + + Examples + -------- + >>> Mouse.insert1({"mouse_id": 1, "dob": "2024-01-15"}) + """ +``` + +### Section Order + +1. Short summary (one line, imperative mood) +2. Extended description +3. Parameters +4. Returns / Yields +5. Raises +6. Examples (strongly encouraged) +7. See Also + +### Style Rules + +- **Do:** Imperative mood ("Insert rows" not "Inserts rows") +- **Do:** Include examples for public APIs +- **Don't:** Document private methods extensively +- **Don't:** Repeat function signature in description + +See [NumPy Docstring Guide](https://numpydoc.readthedocs.io/en/latest/format.html) for full reference. diff --git a/DOCSTRING_STYLE.md b/DOCSTRING_STYLE.md deleted file mode 100644 index 77b6dc90a..000000000 --- a/DOCSTRING_STYLE.md +++ /dev/null @@ -1,499 +0,0 @@ -# DataJoint Python Docstring Style Guide - -This document defines the canonical docstring format for datajoint-python. -All public APIs must follow this NumPy-style format for consistency and -automated documentation generation via mkdocstrings. - -## Quick Reference - -```python -def function(param1, param2, *, keyword_only=None): - """ - Short one-line summary (imperative mood, no period). - - Extended description providing context and details. May span - multiple lines. Explain what the function does, not how. - - Parameters - ---------- - param1 : type - Description of param1. - param2 : type - Description of param2. - keyword_only : type, optional - Description. Default is None. - - Returns - ------- - type - Description of return value. - - Raises - ------ - ExceptionType - When and why this exception is raised. - - Examples - -------- - >>> result = function("value", 42) - >>> print(result) - expected_output - - See Also - -------- - related_function : Brief description. - - Notes - ----- - Additional technical notes, algorithms, or implementation details. - """ -``` - ---- - -## Module Docstrings - -Every module must begin with a docstring explaining its purpose. - -```python -""" -Connection management for DataJoint. - -This module provides the Connection class that manages database connections, -transaction handling, and query execution. It also provides the ``conn()`` -function for accessing a persistent shared connection. - -Key Components --------------- -Connection : class - Manages a single database connection with transaction support. -conn : function - Returns a persistent connection object shared across modules. - -Example -------- ->>> import datajoint as dj ->>> connection = dj.conn() ->>> connection.query("SHOW DATABASES") -""" -``` - ---- - -## Class Docstrings - -```python -class Table(QueryExpression): - """ - Base class for all DataJoint tables. - - Table implements data manipulation (insert, delete, update) and inherits - query functionality from QueryExpression. Concrete table classes must - define the ``definition`` property specifying the table structure. - - Parameters - ---------- - None - Tables are typically instantiated via schema decoration, not directly. - - Attributes - ---------- - definition : str - DataJoint table definition string (DDL). - primary_key : list of str - Names of primary key attributes. - heading : Heading - Table heading with attribute metadata. - - Examples - -------- - Define a table using the schema decorator: - - >>> @schema - ... class Mouse(dj.Manual): - ... definition = ''' - ... mouse_id : int - ... --- - ... dob : date - ... sex : enum("M", "F", "U") - ... ''' - - Insert data: - - >>> Mouse.insert1({"mouse_id": 1, "dob": "2024-01-15", "sex": "M"}) - - See Also - -------- - Manual : Table for manually entered data. - Computed : Table for computed results. - QueryExpression : Query operator base class. - """ -``` - ---- - -## Method Docstrings - -### Standard Method - -```python -def insert(self, rows, *, replace=False, skip_duplicates=False, ignore_extra_fields=False): - """ - Insert one or more rows into the table. - - Parameters - ---------- - rows : iterable - Rows to insert. Each row can be: - - dict: ``{"attr": value, ...}`` - - numpy.void: Record array element - - sequence: Values in heading order - - QueryExpression: Results of a query - - pathlib.Path: Path to CSV file - replace : bool, optional - If True, replace existing rows with matching primary keys. - Default is False. - skip_duplicates : bool, optional - If True, silently skip rows that would cause duplicate key errors. - Default is False. - ignore_extra_fields : bool, optional - If True, ignore fields not in the table heading. - Default is False. - - Returns - ------- - None - - Raises - ------ - DuplicateError - When inserting a row with an existing primary key and neither - ``replace`` nor ``skip_duplicates`` is True. - DataJointError - When required attributes are missing or types are incompatible. - - Examples - -------- - Insert a single row: - - >>> Mouse.insert1({"mouse_id": 1, "dob": "2024-01-15", "sex": "M"}) - - Insert multiple rows: - - >>> Mouse.insert([ - ... {"mouse_id": 2, "dob": "2024-02-01", "sex": "F"}, - ... {"mouse_id": 3, "dob": "2024-02-15", "sex": "M"}, - ... ]) - - Insert from a query: - - >>> TargetTable.insert(SourceTable & "condition > 5") - - See Also - -------- - insert1 : Insert exactly one row. - """ -``` - -### Method with Complex Return - -```python -def fetch(self, *attrs, offset=None, limit=None, order_by=None, format=None, as_dict=False): - """ - Retrieve data from the table. - - Parameters - ---------- - *attrs : str - Attribute names to fetch. If empty, fetches all attributes. - Use "KEY" to fetch primary key as dict. - offset : int, optional - Number of rows to skip. Default is None (no offset). - limit : int, optional - Maximum number of rows to return. Default is None (no limit). - order_by : str or list of str, optional - Attribute(s) to sort by. Use "KEY" for primary key order, - append " DESC" for descending. Default is None (unordered). - format : {"array", "frame"}, optional - Output format when fetching all attributes: - - "array": numpy structured array (default) - - "frame": pandas DataFrame - as_dict : bool, optional - If True, return list of dicts instead of structured array. - Default is False. - - Returns - ------- - numpy.ndarray or list of dict or pandas.DataFrame - Query results in the requested format: - - Single attribute: 1D array of values - - Multiple attributes: tuple of 1D arrays - - No attributes specified: structured array, DataFrame, or list of dicts - - Examples - -------- - Fetch all data as structured array: - - >>> data = Mouse.fetch() - - Fetch specific attributes: - - >>> ids, dobs = Mouse.fetch("mouse_id", "dob") - - Fetch as list of dicts: - - >>> rows = Mouse.fetch(as_dict=True) - >>> for row in rows: - ... print(row["mouse_id"]) - - Fetch with ordering and limit: - - >>> recent = Mouse.fetch(order_by="dob DESC", limit=10) - - See Also - -------- - fetch1 : Fetch exactly one row. - head : Fetch first N rows ordered by key. - tail : Fetch last N rows ordered by key. - """ -``` - -### Generator Method - -```python -def make(self, key): - """ - Compute and insert results for one key. - - This method must be implemented by subclasses of Computed or Imported - tables. It is called by ``populate()`` for each key in ``key_source`` - that is not yet in the table. - - The method can be implemented in two ways: - - **Simple mode** (regular method): - Fetch, compute, and insert within a single transaction. - - **Tripartite mode** (generator method): - Split into ``make_fetch``, ``make_compute``, ``make_insert`` for - long-running computations with deferred transactions. - - Parameters - ---------- - key : dict - Primary key values identifying the entity to compute. - - Yields - ------ - tuple - In tripartite mode, yields fetched data and computed results. - - Raises - ------ - NotImplementedError - If neither ``make`` nor the tripartite methods are implemented. - - Examples - -------- - Simple implementation: - - >>> class ProcessedData(dj.Computed): - ... definition = ''' - ... -> RawData - ... --- - ... result : float - ... ''' - ... - ... def make(self, key): - ... raw = (RawData & key).fetch1("data") - ... result = expensive_computation(raw) - ... self.insert1({**key, "result": result}) - - See Also - -------- - populate : Execute make for all pending keys. - key_source : Query defining keys to populate. - """ -``` - ---- - -## Property Docstrings - -```python -@property -def primary_key(self): - """ - list of str : Names of primary key attributes. - - The primary key uniquely identifies each row in the table. - Derived from the table definition. - - Examples - -------- - >>> Mouse.primary_key - ['mouse_id'] - """ - return self.heading.primary_key -``` - ---- - -## Parameter Types - -Use these type annotations in docstrings: - -| Python Type | Docstring Format | -|-------------|------------------| -| `str` | `str` | -| `int` | `int` | -| `float` | `float` | -| `bool` | `bool` | -| `None` | `None` | -| `list` | `list` or `list of str` | -| `dict` | `dict` or `dict[str, int]` | -| `tuple` | `tuple` or `tuple of (str, int)` | -| Optional | `str or None` or `str, optional` | -| Union | `str or int` | -| Literal | `{"option1", "option2"}` | -| Callable | `callable` | -| Class | `ClassName` | -| Any | `object` | - ---- - -## Section Order - -Sections must appear in this order (include only relevant sections): - -1. **Short Summary** (required) - One line, imperative mood -2. **Deprecation Warning** - If applicable -3. **Extended Summary** - Additional context -4. **Parameters** - Input arguments -5. **Returns** / **Yields** - Output values -6. **Raises** - Exceptions -7. **Warns** - Warnings issued -8. **See Also** - Related functions/classes -9. **Notes** - Technical details -10. **References** - Citations -11. **Examples** (strongly encouraged) - Usage demonstrations - ---- - -## Style Rules - -### Do - -- Use imperative mood: "Insert rows" not "Inserts rows" -- Start with capital letter, no period at end of summary -- Document all public methods -- Include at least one example for public APIs -- Use backticks for code: ``parameter``, ``ClassName`` -- Reference related items in See Also - -### Don't - -- Don't document private methods extensively (brief is fine) -- Don't repeat the function signature in the description -- Don't use "This function..." or "This method..." -- Don't include implementation details in Parameters -- Don't use first person ("I", "we") - ---- - -## Examples Section Best Practices - -```python -""" -Examples --------- -Basic usage: - ->>> table.insert1({"id": 1, "value": 42}) - -With options: - ->>> table.insert(rows, skip_duplicates=True) - -Error handling: - ->>> try: -... table.insert1({"id": 1}) # duplicate -... except dj.errors.DuplicateError: -... print("Already exists") -Already exists -""" -``` - ---- - -## Converting from Sphinx Style - -Replace Sphinx-style docstrings: - -```python -# Before (Sphinx style) -def method(self, param1, param2): - """ - Brief description. - - :param param1: Description of param1. - :type param1: str - :param param2: Description of param2. - :type param2: int - :returns: Description of return value. - :rtype: bool - :raises ValueError: When param1 is empty. - """ - -# After (NumPy style) -def method(self, param1, param2): - """ - Brief description. - - Parameters - ---------- - param1 : str - Description of param1. - param2 : int - Description of param2. - - Returns - ------- - bool - Description of return value. - - Raises - ------ - ValueError - When param1 is empty. - """ -``` - ---- - -## Validation - -Docstrings are validated by: - -1. **mkdocstrings** - Parses for API documentation -2. **ruff** - Linting (D100-D417 rules when enabled) -3. **pytest --doctest-modules** - Executes examples - -Run locally: - -```bash -# Build docs to check parsing -mkdocs build --config-file docs/mkdocs.yaml - -# Check docstring examples -pytest --doctest-modules src/datajoint/ -``` - ---- - -## References - -- [NumPy Docstring Guide](https://numpydoc.readthedocs.io/en/latest/format.html) -- [mkdocstrings Python Handler](https://mkdocstrings.github.io/python/) -- [PEP 257 - Docstring Conventions](https://peps.python.org/pep-0257/) diff --git a/README.md b/README.md index 83e5b1c9f..cb1aca965 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,11 @@ # DataJoint for Python -DataJoint is a framework for scientific data pipelines that introduces the **Relational Workflow Model**—a paradigm where your database schema is an executable specification of your workflow. +DataJoint is a framework for scientific data pipelines based on the **Relational Workflow Model** — a paradigm where your database schema is an executable specification of your workflow. -Traditional databases store data but don't understand how it was computed. DataJoint extends relational databases with native workflow semantics: - -- **Tables represent workflow steps** — Each table is a step in your pipeline where entities are created +- **Tables represent workflow steps** — Each table is a step in your pipeline - **Foreign keys encode dependencies** — Parent tables must be populated before child tables -- **Computations are declarative** — Define *what* to compute; DataJoint determines *when* and tracks *what's done* -- **Results are immutable** — Computed results preserve full provenance and reproducibility - -### Object-Augmented Schemas - -Scientific data includes both structured metadata and large data objects (time series, images, movies, neural recordings, gene sequences). DataJoint solves this with **Object-Augmented Schemas (OAS)**—a unified architecture where relational tables and object storage are managed as one system with identical guarantees for integrity, transactions, and lifecycle. - -### DataJoint 2.0 - -**DataJoint 2.0** solidifies these core concepts with a modernized API, improved type system, and enhanced object storage integration. Existing users can refer to the [Migration Guide](https://docs.datajoint.com/migration/) for upgrading from earlier versions. +- **Computations are declarative** — Define *what* to compute; DataJoint handles *when* +- **Results are immutable** — Full provenance and reproducibility **Documentation:** https://docs.datajoint.com @@ -24,27 +14,19 @@ Scientific data includes both structured metadata and large data objects (time s PyPI - pypi release + pypi Conda - conda-forge release + conda - - Tests - test status - - - Coverage - - - coverage + tests @@ -58,160 +40,42 @@ Scientific data includes both structured metadata and large data objects (time s Citation - DOI + DOI + + + Coverage + + + coverage -## Data Pipeline Example - -![pipeline](https://raw.githubusercontent.com/datajoint/datajoint-python/master/images/pipeline.png) - -[Yatsenko et al., bioRxiv 2021](https://doi.org/10.1101/2021.03.30.437358) - -## Getting Started - -- Install with Conda - - ```bash - conda install -c conda-forge datajoint - ``` - -- Install with pip - - ```bash - pip install datajoint - ``` - -- [Documentation & Tutorials](https://docs.datajoint.com) - -- [DataJoint Elements](https://datajoint.com/docs/elements/) — Catalog of example pipelines for neuroscience experiments - -- [Architecture](ARCHITECTURE.md) — Internal design documentation for contributors - -## Developer Guide - -### Prerequisites - -- [Docker](https://docs.docker.com/get-docker/) (Docker daemon must be running) -- [pixi](https://pixi.sh) (recommended) or Python 3.10+ - -### Quick Start with pixi (Recommended) - -[pixi](https://pixi.sh) manages all dependencies including Python, graphviz, and test tools: - -```bash -# Clone the repo -git clone https://github.com/datajoint/datajoint-python.git -cd datajoint-python - -# Install dependencies and run tests (containers managed by testcontainers) -pixi run test - -# Run with coverage -pixi run test-cov - -# Run pre-commit hooks -pixi run pre-commit run --all-files -``` - -### Running Tests - -Tests use [testcontainers](https://testcontainers.com/) to automatically manage MySQL and MinIO containers. -**No manual `docker-compose up` required** - containers start when tests run and stop afterward. - -```bash -# Run all tests (recommended) -pixi run test - -# Run with coverage report -pixi run test-cov - -# Run only unit tests (no containers needed) -pixi run -e test pytest tests/unit/ - -# Run specific test file -pixi run -e test pytest tests/integration/test_blob.py -v -``` - -**macOS Docker Desktop users:** If tests fail to connect to Docker, set `DOCKER_HOST`: -```bash -export DOCKER_HOST=unix://$HOME/.docker/run/docker.sock -``` - -### Alternative: Using pip - -If you prefer pip over pixi: +## Installation ```bash -pip install -e ".[test]" -pytest tests/ +pip install datajoint ``` -### Alternative: External Containers - -For development/debugging, you may prefer persistent containers that survive test runs: - -```bash -# Start containers manually -docker compose up -d db minio - -# Run tests using external containers -DJ_USE_EXTERNAL_CONTAINERS=1 pixi run test -# Or with pip: DJ_USE_EXTERNAL_CONTAINERS=1 pytest tests/ - -# Stop containers when done -docker compose down -``` - -### Alternative: Full Docker - -Run tests entirely in Docker (no local Python needed): - -```bash -docker compose --profile test up djtest --build -``` - -### Pre-commit Hooks - -Pre-commit hooks run automatically on `git commit` to check code quality. -**All hooks must pass before committing.** +or with Conda: ```bash -# Install hooks (first time only) -pixi run pre-commit install -# Or with pip: pip install pre-commit && pre-commit install - -# Run all checks manually -pixi run pre-commit run --all-files - -# Run specific hook -pixi run pre-commit run ruff --all-files +conda install -c conda-forge datajoint ``` -Hooks include: -- **ruff**: Python linting and formatting -- **codespell**: Spell checking -- **YAML/JSON/TOML validation** -- **Large file detection** +## Example Pipeline -### Before Submitting a PR +![pipeline](https://raw.githubusercontent.com/datajoint/datajoint-python/master/images/pipeline.png) -1. **Run all tests**: `pixi run test` -2. **Run pre-commit**: `pixi run pre-commit run --all-files` -3. **Check coverage**: `pixi run test-cov` +[Yatsenko et al., bioRxiv 2021](https://doi.org/10.1101/2021.03.30.437358) -### Environment Variables +## Resources -For external container mode (`DJ_USE_EXTERNAL_CONTAINERS=1`): +- [Documentation & Tutorials](https://docs.datajoint.com) +- [DataJoint Elements](https://datajoint.com/docs/elements/) — Example pipelines for neuroscience +- [GitHub Discussions](https://github.com/datajoint/datajoint-python/discussions) -| Variable | Default | Description | -|----------|---------|-------------| -| `DJ_HOST` | `localhost` | MySQL hostname | -| `DJ_PORT` | `3306` | MySQL port | -| `DJ_USER` | `root` | MySQL username | -| `DJ_PASS` | `password` | MySQL password | -| `S3_ENDPOINT` | `localhost:9000` | MinIO endpoint | +## Contributing -For Docker-based testing (devcontainer, djtest), set `DJ_HOST=db` and `S3_ENDPOINT=minio:9000`. +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines. From 78f4d0c6f39f082cedcb20353da63dc63b391dba Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 11 Jan 2026 13:43:59 -0600 Subject: [PATCH 218/219] docs: Add RFC discussion template; fix table header styling - Add .github/DISCUSSION_TEMPLATE/rfc.yml for enhancement proposals - Fix table header alignment (center instead of right) - Fix excessive padding in table headers by removing p tag margins Co-Authored-By: Claude Opus 4.5 --- .github/DISCUSSION_TEMPLATE/rfc.yml | 107 ++++++++++++++++++++++++++++ src/datajoint/preview.py | 9 ++- 2 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 .github/DISCUSSION_TEMPLATE/rfc.yml diff --git a/.github/DISCUSSION_TEMPLATE/rfc.yml b/.github/DISCUSSION_TEMPLATE/rfc.yml new file mode 100644 index 000000000..53dbecded --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/rfc.yml @@ -0,0 +1,107 @@ +title: "[RFC] " +labels: + - rfc + - "status: proposed" +body: + - type: markdown + attributes: + value: | + ## DataJoint Enhancement Proposal + + Use this template to propose changes to DataJoint specifications, APIs, or documentation structure. + + **Before submitting:** + - Search existing discussions to avoid duplicates + - Consider starting with an informal discussion in the Ideas category first + + - type: textarea + id: summary + attributes: + label: Summary + description: A brief, one-paragraph explanation of the proposal. + placeholder: This proposal adds/changes/removes... + validations: + required: true + + - type: textarea + id: motivation + attributes: + label: Motivation + description: | + Why is this change needed? What problem does it solve? + Include concrete use cases and examples where possible. + placeholder: | + Currently, users need to... + This causes problems when... + With this change, users could... + validations: + required: true + + - type: textarea + id: design + attributes: + label: Proposed Design + description: | + Detailed explanation of the proposed solution. + Include code examples, API signatures, or schema definitions as appropriate. + placeholder: | + ## API Changes + ```python + # Example usage + ``` + + ## Behavior + - When X happens, Y should occur + - Error handling: ... + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: What other approaches were considered and why were they not chosen? + placeholder: | + 1. Alternative A: ... + Rejected because: ... + + 2. Alternative B: ... + Rejected because: ... + + - type: textarea + id: compatibility + attributes: + label: Backwards Compatibility + description: | + How does this affect existing users? + - Breaking changes? + - Migration path? + - Deprecation timeline? + placeholder: | + This change is/is not backwards compatible. + + Migration path: + 1. ... + + - type: textarea + id: implementation + attributes: + label: Implementation Notes + description: | + Optional: Technical details, affected files, estimated scope. + Prototyping in parallel with RFC discussion is encouraged. + placeholder: | + Affected components: + - datajoint-python/src/datajoint/... + + Estimated scope: small/medium/large + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I have searched existing discussions and issues for duplicates + required: true + - label: I have considered backwards compatibility + required: true diff --git a/src/datajoint/preview.py b/src/datajoint/preview.py index 5e2c92e5f..8cca46b2a 100644 --- a/src/datajoint/preview.py +++ b/src/datajoint/preview.py @@ -122,7 +122,10 @@ def get_html_display_value(tup, name, idx): } .Table th{ background: #A0A0A0; color: #ffffff; padding:2px 4px; border:#f0e0e0 1px solid; - font-weight: normal; font-family: monospace; font-size: 75%; + font-weight: normal; font-family: monospace; font-size: 75%; text-align: center; + } + .Table th p{ + margin: 0; } .Table td{ padding:2px 4px; border:#f0e0e0 1px solid; font-size: 75%; @@ -168,7 +171,7 @@ def get_html_display_value(tup, name, idx): /* Dark mode support */ @media (prefers-color-scheme: dark) { .Table th{ - background: #4a4a4a; color: #ffffff; border:#555555 1px solid; + background: #4a4a4a; color: #ffffff; border:#555555 1px solid; text-align: center; } .Table td{ border:#555555 1px solid; @@ -203,7 +206,7 @@ def get_html_display_value(tup, name, idx): {title}
- + {body}
{head}
{head}
{ellipsis} From 7825b3c6f019fe501c1fbfd534e53e00766750d1 Mon Sep 17 00:00:00 2001 From: Dimitri Yatsenko Date: Sun, 11 Jan 2026 18:19:52 -0600 Subject: [PATCH 219/219] enhance: Show codec names in table preview instead of =BLOB= - Raw blobs (no codec) now show "bytes" - Raw json (no codec) shows "json" - Codec fields show "" (e.g., , , ) - HTML output properly escapes angle brackets for browser display - Improves clarity when viewing table contents Example output: *id raw_blob blob_data json_data 1 bytes json Co-Authored-By: Claude Opus 4.5 --- src/datajoint/preview.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/datajoint/preview.py b/src/datajoint/preview.py index 8cca46b2a..c0f103eb1 100644 --- a/src/datajoint/preview.py +++ b/src/datajoint/preview.py @@ -24,6 +24,21 @@ def _format_object_display(json_data): return "=OBJ[file]=" +def _get_blob_placeholder(heading, field_name, html_escape=False): + """Get display placeholder for a blob/json field based on its codec.""" + attr = heading.attributes.get(field_name) + if attr is None: + return "bytes" + if attr.codec is not None: + name = attr.codec.name + if html_escape: + return f"<{name}>" + return f"<{name}>" + if attr.json: + return "json" + return "bytes" + + def preview(query_expression, limit, width): heading = query_expression.heading rel = query_expression.proj(*heading.non_blobs) @@ -55,7 +70,7 @@ def preview(query_expression, limit, width): def get_placeholder(f): if f in object_fields: return "=OBJ[.xxx]=" - return "=BLOB=" + return _get_blob_placeholder(heading, f) widths = { f: min( @@ -72,7 +87,7 @@ def get_display_value(tup, f, idx): elif f in object_fields and idx < len(object_data_list): return _format_object_display(object_data_list[idx].get(f)) else: - return "=BLOB=" + return _get_blob_placeholder(heading, f) return ( " ".join([templates[f] % ("*" + f if f in rel.primary_key else f) for f in columns]) @@ -113,7 +128,7 @@ def get_html_display_value(tup, name, idx): elif name in object_fields and idx < len(object_data_list): return _format_object_display(object_data_list[idx].get(name)) else: - return "=BLOB=" + return _get_blob_placeholder(heading, name, html_escape=True) css = """