diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index 3e1129a..77064e8 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -70,6 +70,5 @@ Every step says who does it: script (run the command, read the result), CI (a wo uv run --no-project python scripts/bump_engine.py --engine --dev-version --check ``` - - [ ] 9. CI, then owner: merge the bump PR when every check is green, and delete the branch and the worktree. Then remove the tests and skip-list lines that waited for the new engine: `git grep -n -i "native build links\|native image links"`. - [ ] 10. owner: check the GitHub Release (notes, pre-release flag) and, after a final release, that the milestone is closed and what was still open in it moved to the next milestone (the open one due next, or one the workflow created named after the next upstream engine version). If the run failed with a message about the next milestone, create it with a due date and re-run `GitHub Release` (Actions, `workflow_dispatch`, the tag). Close this issue with the two run ids and the two PyPI links. diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..1d5e047 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,52 @@ +# Enforces the pre-commit hooks (.pre-commit-config.yaml) on every pull request and push to main, +# so a contribution made without the local hooks is still checked. Contributors run the same hooks +# locally with `uvx pre-commit install` (see CONTRIBUTING.md). +name: Pre-commit + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + pre-commit: + name: Run pre-commit on all files + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + + # pre-commit and its hooks run through uvx (a throwaway environment), so nothing is installed + # into the runner's Python. uv is pinned in every install URL of this repo. + - name: Setup UV package manager + shell: bash + run: | + # Retry transient network failures (a connection reset failed a whole examples job on #8897); the + # script is downloaded first so a retried transfer is never piped half-written into sh. + installer="$(mktemp)" + curl -LsSf --retry 5 --retry-all-errors --retry-delay 5 -o "$installer" https://astral.sh/uv/0.12.0/install.sh + sh "$installer" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.local/bin:$PATH" + uv --version + + - name: Run pre-commit on all files + env: + # shellcheck and hadolint findings are fixed in the follow-up PR. + SKIP: shellcheck,hadolint-docker + run: uvx pre-commit run --all-files --show-diff-on-failure diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 501296e..255ce3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,19 @@ repos: + # Basic file hygiene (validate and normalize), matching upstream ArcadeDB's pre-commit-hooks set. +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-added-large-files + - id: detect-private-key + - id: check-yaml + args: [--allow-multiple-documents] + - id: check-toml + - id: check-json + - id: check-xml + # Python formatting (unchanged): black + isort with the black profile. - repo: https://github.com/psf/black rev: 26.10.1 hooks: @@ -9,6 +24,32 @@ repos: hooks: - id: isort args: [--profile, black] + # Python linting: ruff with its default rules (E4, E7, E9, F) at line length 88 (pyproject.toml). + # Check only - fixes land in their own commit; black and isort remain the formatters. +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.17.4 + hooks: + - id: ruff-check + # Java, XML, Markdown and JSON formatting, matching upstream ArcadeDB (printWidth 160 for + # Java/XML is set in .prettierrc.json; Markdown prose is not hard-wrapped). types_or (OR) is + # required: types (AND) of [java, xml] would match no file. +- repo: https://github.com/rbubley/mirrors-prettier + rev: v3.9.10 + hooks: + - id: prettier + types_or: [java, xml, markdown, json] + additional_dependencies: + - prettier@3.9.10 + - prettier-plugin-java@2.11.0 + - prettier-plugin-xml + args: [--write, --list-different, --ignore-unknown] + # Shell formatting (unchanged). +- repo: https://github.com/scop/pre-commit-shfmt + rev: v3.14.1-1 + hooks: + - id: shfmt + args: [-i, '4', -ci, -sr, -w] + # YAML formatting (unchanged): workflows are linted by actionlint, not reformatted (as upstream). - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.16.0 hooks: @@ -16,18 +57,22 @@ repos: args: - --autofix - --indent=2 - # Workflows are linted by actionlint, not reformatted (as upstream). exclude: ^\.github/ -- repo: https://github.com/scop/pre-commit-shfmt - rev: v3.14.1-1 + # Shell linting (.sh files). Runs in Docker; actionlint already runs shellcheck over workflow run: blocks. +- repo: https://github.com/koalaman/shellcheck-precommit + rev: v0.11.0 hooks: - - id: shfmt - args: [-i, '4', -ci, -sr, -w] - # actionlint runs shellcheck over every `run:` block; per-file ignores live in .github/actionlint.yaml. + - id: shellcheck + # actionlint runs shellcheck over every run: block; per-file ignores live in .github/actionlint.yaml. - repo: https://github.com/rhysd/actionlint rev: v1.7.12 hooks: - id: actionlint + # Dockerfile linting (scripts/Dockerfile.build). Runs in Docker. +- repo: https://github.com/hadolint/hadolint + rev: v2.15.1 + hooks: + - id: hadolint-docker - repo: local hooks: - id: no-old-package-name diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..25dfb35 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +# Files that prettier must not reformat. +# The notices file and the two JDK notice texts are produced by +# native/wheel/gen_notices.py; reachability-metadata.json is produced by +# the GraalVM native-image build. Reformatting them would cause the +# notices drift check (native wheel build) to fail. +native/wheel/THIRD_PARTY_NOTICES.md +native/wheel/notice-texts/ +native/src/main/resources/META-INF/native-image/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0dcd006 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "printWidth": 80, + "proseWrap": "preserve", + "overrides": [ + { + "files": ["**/*.java", "**/*.xml"], + "options": { "printWidth": 160 } + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f19c318..03fb7ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,20 +87,23 @@ uv run --no-project --python 3.12 python native/wheel/build_wheel.py --lib-dir < ### Pre-commit -This project uses [pre-commit](https://pre-commit.com/) (black, isort, shfmt, pretty-format-yaml, actionlint, and a local check that the old package name does not come back). Run it on the files you changed before you push, and again if it reformatted something: +This project uses [pre-commit](https://pre-commit.com/): ruff (lint, default rules E4/E7/E9/F at line length 88), black and isort (Python formatting), prettier (Java, XML, Markdown, JSON), shfmt and shellcheck (shell), hadolint (Dockerfile), pretty-format-yaml and the basic hygiene hooks (trailing whitespace, end-of-file, merge-conflict markers, large files, private keys), and actionlint (GitHub Actions). The settings live in `pyproject.toml` (`[tool.ruff]`) and `.prettierrc.json`. + +Install the hooks once so they run on every commit: ```shell -uvx pre-commit run --files -uvx pre-commit run --all-files +uvx pre-commit install ``` -CI also runs Bandit, which must be clean: +Run them on the files you changed before you push, and again if something was reformatted: ```shell -uvx bandit==1.9.4 -c pyproject.toml -r src tests --severity-level low --confidence-level low -uvx bandit==1.9.4 -c pyproject.toml -r examples --severity-level medium --confidence-level high +uvx pre-commit run --files +uvx pre-commit run --all-files ``` +shellcheck and hadolint run as `docker_image` hooks (Docker pulls the images automatically, both locally and in CI). CI runs the same hooks on every pull request and push. + ### Workflow 1. An [issue](https://github.com/ArcadeData/arcadedb-python/issues) states the problem. diff --git a/README.md b/README.md index eec173d..631b796 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,10 @@ Python bindings for [ArcadeDB](https://arcadedb.com), the multi-model database (graph, document, key/value, vector and time series, queried with SQL or openCypher). The engine runs inside your Python process, with no server and no network hop. This repository builds two packages, and both import as `arcadedb`: `arcadedb` (the native build, the default) for scripts, notebooks, tests, CLIs, serverless functions and agents, and `arcadedb-jvm` (the JVM build) for heavy, long-running loads. Status: beta. -| Package | Engine | Pick it for | -|---|---|---| -| [`arcadedb`](https://pypi.org/project/arcadedb/) (native, the default) | The engine compiled by GraalVM Community Edition into a shared library, loaded with `ctypes`. No JVM, no JPype | Scripts, notebooks, tests, CLIs, serverless functions, agents | -| [`arcadedb-jvm`](https://pypi.org/project/arcadedb-jvm/) (JVM) | The Java engine in your process through [JPype](https://jpype.readthedocs.io/), with a bundled Java 25 runtime | Heavy, long-running loads, where the JIT wins over time. Also the Java-object APIs and JVM options | +| Package | Engine | Pick it for | +| ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| [`arcadedb`](https://pypi.org/project/arcadedb/) (native, the default) | The engine compiled by GraalVM Community Edition into a shared library, loaded with `ctypes`. No JVM, no JPype | Scripts, notebooks, tests, CLIs, serverless functions, agents | +| [`arcadedb-jvm`](https://pypi.org/project/arcadedb-jvm/) (JVM) | The Java engine in your process through [JPype](https://jpype.readthedocs.io/), with a bundled Java 25 runtime | Heavy, long-running loads, where the JIT wins over time. Also the Java-object APIs and JVM options | Both packages `import arcadedb`, and they have the same public API. Install one, not both. Not sure which? Read [Which package?](https://github.com/ArcadeData/arcadedb-python/blob/main/docs/modules/ROOT/pages/which-package.adoc). @@ -20,12 +20,12 @@ Both packages `import arcadedb`, and they have the same public API. Install one, This repository is the embedded bindings: the engine runs inside your Python process. The official [drivers](https://github.com/ArcadeData/arcadedb-drivers) are clients of a running ArcadeDB server. Do not mix them up. -| | Embedded bindings (this repository) | Drivers | -|---|---|---| -| What it is | The ArcadeDB engine, inside your Python process | Python clients of an ArcadeDB server, HTTP (sync and async) or gRPC. They contain no engine | -| Needs a server? | No. An in-process HTTP server and Studio are optional | Yes: the Docker image `arcadedata/arcadedb` or any ArcadeDB server. The gRPC driver also needs the server's gRPC plugin | -| Package and import | `arcadedb` or `arcadedb-jvm`, `import arcadedb` | [`arcadedb-driver`](https://pypi.org/project/arcadedb-driver/), `import arcadedb_driver` (HTTP); [`arcadedb-driver-grpc`](https://pypi.org/project/arcadedb-driver-grpc/), `import arcadedb_driver_grpc` (gRPC) | -| Use it when | One process, no server: tests, notebooks, scripts, an agent that owns its data | Several clients, an existing server, a service boundary, HA or replication, or clients that are not Python | +| | Embedded bindings (this repository) | Drivers | +| ------------------ | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| What it is | The ArcadeDB engine, inside your Python process | Python clients of an ArcadeDB server, HTTP (sync and async) or gRPC. They contain no engine | +| Needs a server? | No. An in-process HTTP server and Studio are optional | Yes: the Docker image `arcadedata/arcadedb` or any ArcadeDB server. The gRPC driver also needs the server's gRPC plugin | +| Package and import | `arcadedb` or `arcadedb-jvm`, `import arcadedb` | [`arcadedb-driver`](https://pypi.org/project/arcadedb-driver/), `import arcadedb_driver` (HTTP); [`arcadedb-driver-grpc`](https://pypi.org/project/arcadedb-driver-grpc/), `import arcadedb_driver_grpc` (gRPC) | +| Use it when | One process, no server: tests, notebooks, scripts, an agent that owns its data | Several clients, an existing server, a service boundary, HA or replication, or clients that are not Python | You can also use both, for example the bindings for a job and the HTTP driver for a shared server. Driver docs: [HTTP](https://docs.arcadedb.com/arcadedb/how-to/connectivity/drivers/python-http), [gRPC](https://docs.arcadedb.com/arcadedb/how-to/connectivity/drivers/python-grpc). More in [Which package?](https://github.com/ArcadeData/arcadedb-python/blob/main/docs/modules/ROOT/pages/which-package.adoc#embedded-or-driver). @@ -37,11 +37,13 @@ uv add arcadedb-jvm # JVM, for heavy long-running loads ``` + > The first release of this layout is 26.11.1, and both packages have a final release on PyPI. A plain `uv add arcadedb` installs the final, and a later pre-release (`A.B.C.devN`) needs `uv add --prerelease allow arcadedb`. The same holds for `arcadedb-jvm`. > > A `.devN` version (for example `26.12.1.dev0`, when one is published) is a development pre-release. It embeds a snapshot of the upstream engine, the same snapshot in both packages, pinned by image digest and jar checksums in `scripts/dev_engine_pin.json`, and the wheel states it (`arcadedb.ENGINE_SNAPSHOT`, `arcadedb.ENGINE_IMAGE_DIGEST`). Install it with `uv add --prerelease allow arcadedb==26.12.1.dev0` (or `arcadedb-jvm==26.12.1.dev0`). + > Old names: `arcadedb-jvm` was formerly `arcadedb-embedded`, and `arcadedb` was formerly `arcadedb-embedded-native` (archived on PyPI). The last `arcadedb-embedded` release, 26.11.1, is a deprecation shim: it installs `arcadedb`, the native build, and warns once at import. It does not install `arcadedb-jvm`. - Python: `arcadedb` (native) is tested on 3.11 to 3.14 and also installs on 3.15 (`requires-python` is `>=3.11` with no upper bound; the CI runs a subset of the tests on 3.15). `arcadedb-jvm` needs 3.11 to 3.14 (`requires-python` is `>=3.11,<3.15`; its dependency JPype has no 3.15 wheel yet). Each release builds and tests both packages on Python 3.11 to 3.14 on all four platforms below. Between releases, `main` and the nightly run test `arcadedb` on 3.12 and 3.14 on every platform (with a short subset on 3.11, 3.13 and 3.15), and `arcadedb-jvm` on 3.11 to 3.14 on Linux x86-64 and on 3.11 and 3.14 on the other three. A pull request runs a smaller selection. @@ -80,16 +82,18 @@ This prints `Name: Alice` (the engine also logs to standard error). `create_data - An optional in-process HTTP server (`create_server()`), off until you call it, for other processes that need the same data. + > New in 26.11.1: the `arcadedb-jvm` wheels carry the Studio web UI, which the server then serves. Studio used to bundle ApexCharts, which is no longer open source ([ArcadeData/arcadedb#10579](https://github.com/ArcadeData/arcadedb/issues/10579)), and [ArcadeData/arcadedb#10627](https://github.com/ArcadeData/arcadedb/pull/10627) replaced it with Apache ECharts. The earlier 26.10.x wheels on PyPI have no Studio. ## Packages on PyPI -| Package | What it is | Status | -|---|---|---| -| [`arcadedb`](https://pypi.org/project/arcadedb/) | The native build: the engine compiled by GraalVM Community Edition, no JVM, no JPype. The default for embedded use | First release of this layout: 26.11.1 | +| Package | What it is | Status | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [`arcadedb`](https://pypi.org/project/arcadedb/) | The native build: the engine compiled by GraalVM Community Edition, no JVM, no JPype. The default for embedded use | First release of this layout: 26.11.1 | | [`arcadedb-jvm`](https://pypi.org/project/arcadedb-jvm/) | The JVM build: the Java engine on a bundled JVM, through JPype. The package this README described before the two-package layout | First release from this repository: 26.11.1 | + Formerly `arcadedb-embedded` (now `arcadedb-jvm`, the same build) and formerly `arcadedb-embedded-native` (now `arcadedb`). They imported as `arcadedb_embedded` and `arcadedb_native`, and they are deprecated: `arcadedb-embedded-native` is archived, and the last `arcadedb-embedded` release, 26.11.1, is a shim that installs `arcadedb` and warns once at import. Releases from this repository start with 26.11.1. The 26.10.x wheels were built and published from [humemai/arcadedb-embedded-python](https://github.com/humemai/arcadedb-embedded-python). diff --git a/SECURITY.md b/SECURITY.md index 54ee947..fc5f89e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -33,11 +33,11 @@ To help us triage quickly, please include: Security fixes are delivered in a new release of the package. Both packages follow the version of the ArcadeDB engine they ship: -| Version | Meaning | -| ------- | ------- | -| `A.B.C` | The release. The wheels ship the `A.B.C` engine. | +| Version | Meaning | +| ------------- | ------------------------------------------------------------------------------------------- | +| `A.B.C` | The release. The wheels ship the `A.B.C` engine. | | `A.B.C.postN` | A fix in the Python code of the released `A.B.C`. The wheels still ship the `A.B.C` engine. | -| `A.B.C.devN` | A preview of the engine release `A.B.C`, built from upstream snapshot jars. | +| `A.B.C.devN` | A preview of the engine release `A.B.C`, built from upstream snapshot jars. | The numbering rule is on the docs page [Release](docs/modules/ROOT/pages/development/release.adoc). diff --git a/examples/01_simple_document_store.py b/examples/01_simple_document_store.py index c982c05..3903508 100644 --- a/examples/01_simple_document_store.py +++ b/examples/01_simple_document_store.py @@ -35,7 +35,6 @@ import shutil import time import uuid -from datetime import date, datetime import arcadedb @@ -277,7 +276,7 @@ if not tasks: print(" (none found)") else: - print(f" 💡 Used to_list() for efficient bulk conversion") + print(" 💡 Used to_list() for efficient bulk conversion") print() diff --git a/examples/06_vector_search_recommendations.py b/examples/06_vector_search_recommendations.py index 0001f3f..7630879 100644 --- a/examples/06_vector_search_recommendations.py +++ b/examples/06_vector_search_recommendations.py @@ -41,14 +41,11 @@ """ import argparse -import os import shutil import sys import time from pathlib import Path -import numpy as np - import arcadedb # Try to import sentence_transformers @@ -114,7 +111,6 @@ def generate_embeddings( # Check if embeddings already exist try: - count = db.count_type("Movie") # Check if property exists and has values query = ( f"SELECT count(*) as count FROM Movie WHERE {embedding_prop} IS NOT NULL" diff --git a/examples/08_stackoverflow_tables_olap.py b/examples/08_stackoverflow_tables_olap.py index f94a7cd..2ac3f1a 100644 --- a/examples/08_stackoverflow_tables_olap.py +++ b/examples/08_stackoverflow_tables_olap.py @@ -1972,7 +1972,9 @@ def run_olap_postgresql( conn = psycopg.connect("dbname=bench user=postgres host=127.0.0.1") - pid_provider = lambda: list_postgres_pids(pgdata) + def pid_provider() -> List[int]: + return list_postgres_pids(pgdata) + stop_event, rss_state, rss_thread = start_combined_rss_sampler(pid_provider) print("Creating schema...") diff --git a/examples/13_stackoverflow_hybrid_queries.py b/examples/13_stackoverflow_hybrid_queries.py index 00bb0f4..e1c94a3 100644 --- a/examples/13_stackoverflow_hybrid_queries.py +++ b/examples/13_stackoverflow_hybrid_queries.py @@ -1944,7 +1944,7 @@ def run_hybrid_queries( "cypher_expand_users", lambda: run_cypher( db, - f""" + """ MATCH (q:Question)-[:HAS_ANSWER]->(a:Answer)<-[:ANSWERED]-(u:User) WHERE q.Id IN $ids RETURN DISTINCT u.Id AS user_id diff --git a/examples/README.md b/examples/README.md index 0133c5c..ad37c33 100644 --- a/examples/README.md +++ b/examples/README.md @@ -22,9 +22,11 @@ Examples 02, 03, 14, 15 and 25 use Java objects and run on `arcadedb-jvm` only ( ## Available Examples ### 📄 [01_simple_document_store.py](./01_simple_document_store.py) + **Document Types | CRUD Operations | Rich Data Types | NULL Handling** Perfect introduction to ArcadeDB basics: + - Creating embedded databases (no server needed) - Document types with comprehensive schema (STRING, BOOLEAN, DATE, DATETIME, DECIMAL, FLOAT, INTEGER, LIST, etc.) - CRUD operations with ArcadeDB SQL @@ -38,9 +40,11 @@ Perfect introduction to ArcadeDB basics: --- ### 🔗 [02_social_network_graph.py](./02_social_network_graph.py) + **Vertex Types | Edge Types | Graph Traversal | OpenCypher vs SQL MATCH | NULL Handling** Complete social network modeling with graph database: + - Creating vertex types (Person) and edge types (FRIEND_OF) with rich properties - NULL value handling for optional fields (email, phone, reputation) - Bidirectional relationships with metadata (since, closeness) @@ -53,9 +57,11 @@ Complete social network modeling with graph database: **Learn:** Graph schema design, relationship modeling, multi-language querying, NULL handling in graphs ### 🔍 [03_vector_search.py](./03_vector_search.py) + **Vector Embeddings | HNSW (JVector) Index | Semantic Search | Performance Analysis** Semantic similarity search with AI/ML: + - Creating vector-ready schema (`ARRAY_OF_FLOATS`) for embeddings - Generating mock embeddings clustered by category (seeded with `zlib.crc32`, so every run builds the same embeddings) @@ -68,9 +74,11 @@ Semantic similarity search with AI/ML: --- ### 📄 [04_csv_import_documents.py](./04_csv_import_documents.py) + **CSV Import | Schema Definition | Batch Processing | Explicit Schema Mapping** High-performance CSV import for document data: + - Importing the MovieLens dataset (movies, ratings, links, and tags) - Explicit schema mapping (integer-like columns to LONG, decimals to DOUBLE, text to STRING) and batched parameterized `INSERT` @@ -85,9 +93,11 @@ High-performance CSV import for document data: --- ### 🕸️ [05_csv_import_graph.py](./05_csv_import_graph.py) + **Graph Import | Edge Creation | Foreign Keys | Performance Benchmarking** Complex graph construction from CSV data: + - Importing Users, Movies, and Ratings - Creating edges (User-[RATED]->Movie) from foreign keys - Handling large-scale edge creation (millions of edges) @@ -99,9 +109,11 @@ Complex graph construction from CSV data: --- ### 🎬 [06_vector_search_recommendations.py](./06_vector_search_recommendations.py) + **Recommendation Engine | Vector Search vs Graph Traversal | Real-world Use Case** Building a movie recommendation engine: + - Generating embeddings for movies (Title + Genres) with two sentence-transformers models - "More like this" recommendations for a given movie - Graph-based collaborative filtering (movies rated highly by users who liked the query @@ -114,9 +126,11 @@ similarity differ on the same data --- ### 🗃️ [07_stackoverflow_tables_oltp.py](./07_stackoverflow_tables_oltp.py) + **Table OLTP | Mixed CRUD Workload | Cross-DB Benchmarking** Table-oriented Stack Overflow OLTP benchmark: + - Loads Stack Overflow XML tables and runs a mixed OLTP workload (CRUD) - CRUD operations are point-oriented and randomly target one table per operation - For cross-database comparability, running with `--threads 1` is recommended @@ -126,9 +140,11 @@ Table-oriented Stack Overflow OLTP benchmark: --- ### 📉 [08_stackoverflow_tables_olap.py](./08_stackoverflow_tables_olap.py) + **Table OLAP | Fixed Query Suite | Dockerized Runs** Table-oriented Stack Overflow OLAP benchmark: + - Loads all Stack Overflow XML tables and runs a fixed OLAP query suite - Re-launches itself in a Docker container when `docker` is available (and, for ArcadeDB, a wheel is present in `dist`); otherwise it runs natively, @@ -140,9 +156,11 @@ Table-oriented Stack Overflow OLAP benchmark: --- ### 📊 [09_stackoverflow_graph_oltp.py](./09_stackoverflow_graph_oltp.py) + **Graph OLTP | Deterministic Verification | Cross-DB Benchmarking** Stack Overflow property-graph OLTP benchmark with mixed CRUD operations: + - Supports deterministic single-thread verification through DB-scoped baselines - `--verify-single-thread-series` checks repeatability for one DB/mode, not strict cross-DB equality @@ -156,9 +174,11 @@ Stack Overflow property-graph OLTP benchmark with mixed CRUD operations: --- ### 📈 [10_stackoverflow_graph_olap.py](./10_stackoverflow_graph_olap.py) + **Graph OLAP | OpenCypher Query Suite | Cross-DB Benchmarking** Fixed query-suite benchmark for Stack Overflow graph analytics: + - ArcadeDB preloads through `db.graph_batch(...)` with two-way edges - ArcadeDB query execution is Cypher-only in this example path @@ -167,9 +187,11 @@ Fixed query-suite benchmark for Stack Overflow graph analytics: --- ### 🏗️ [11_vector_index_build.py](./11_vector_index_build.py) + **Vector Index Build | Build-Only Benchmark | Multi-Backend Comparison** Build-only vector index benchmark: + - Backends: ArcadeDB (embedded), pgvector, Qdrant, Milvus, FAISS, LanceDB - Datasets: MSMARCO and Stack Overflow embedding vectors - For client-server backends, peak RSS combines client process and server process tree @@ -179,9 +201,11 @@ Build-only vector index benchmark: --- ### 🔎 [12_vector_search.py](./12_vector_search.py) + **Vector Search | Search-Only Benchmark | ef_search Sweeps** Search-only vector benchmark that reuses Example 11 output: + - Backends: ArcadeDB (embedded), FAISS, LanceDB, brute-force NumPy scan, pgvector, Qdrant, Milvus - Sweeps explicit `ef_search` values for HNSW-style backends - Measures client + server process RSS for client-server backends @@ -191,10 +215,12 @@ Search-only vector benchmark that reuses Example 11 output: --- ### 🔀 [13_stackoverflow_hybrid_queries.py](./13_stackoverflow_hybrid_queries.py) + **Hybrid SQL + Graph + Vector | Standalone Workflow** Standalone Stack Overflow workflow combining documents, graph edges, embeddings, and hybrid queries: + - Preload uses synchronous transaction-batched ingest rather than `IMPORT DATABASE` - Graph edge creation uses RID-based directed endpoints @@ -203,9 +229,11 @@ hybrid queries: --- ### ⏱️ [14_lifecycle_timing.py](./14_lifecycle_timing.py) + **JVM Startup | DB Create/Open | Transaction Load | Query Phases | Reopen Timing** Lifecycle benchmark for embedded ArcadeDB with mixed workloads: + - Measures JVM startup time in-process - Creates schema and loads table/graph/vector data - Runs query workload before and after reopen @@ -217,9 +245,11 @@ Lifecycle benchmark for embedded ArcadeDB with mixed workloads: --- ### 🚚 [15_import_database_vs_transactional_table_ingest.py](./15_import_database_vs_transactional_table_ingest.py) + **Transactional SQL vs Async SQL vs SQL Import | Table Ingest Benchmark** Synthetic multi-table ingest comparison harness: + - Runs four modes against the same generated dataset shape - Modes: transactional SQL, async SQL, SQL `IMPORT DATABASE`, and `db.import_documents(...)` - Includes parity checks so final table counts must match before timing results should @@ -234,9 +264,11 @@ Synthetic multi-table ingest comparison harness: --- ### 🌐 [16_import_database_vs_transactional_graph_ingest.py](./16_import_database_vs_transactional_graph_ingest.py) + **Transactional SQL vs Async SQL vs SQL Import | Graph Ingest Benchmark** Synthetic graph ingest comparison harness: + - Runs transactional SQL, GraphBatch, async SQL, and SQL import on equivalent vertex/edge data - Includes parity checks on final vertex and edge counts @@ -248,9 +280,11 @@ Synthetic graph ingest comparison harness: --- ### ⏱️ [17_timeseries_end_to_end.py](./17_timeseries_end_to_end.py) + **Time Series SQL | Tagged Points | Range Queries | Bucket Aggregation** SQL-first time-series workflow from Python bindings: + - Creates a `TIMESERIES TYPE` with timestamp, tags, and numeric fields - Inserts tagged samples inside a transaction - Queries a time range for a single sensor @@ -262,9 +296,11 @@ SQL-first time-series workflow from Python bindings: --- ### 🌍 [18_geo_predicates_wkt.py](./18_geo_predicates_wkt.py) + **GEOSPATIAL Indexes | WKT Geometry | Indexed Predicates | Fallback Queries** SQL-first geospatial workflow: + - stores points and polygons as WKT - builds a GEOSPATIAL index - runs indexed `within` and `intersects` predicates @@ -275,9 +311,11 @@ SQL-first geospatial workflow: --- ### #️⃣ [19_hash_index_exact_match.py](./19_hash_index_exact_match.py) + **HASH Index | Unique And Non-Unique Lookup | Schema Inspection | Duplicate Rejection** SQL-first exact-match workflow: + - builds unique and non-unique HASH indexes - exercises exact-match lookup semantics for present and missing keys - inspects created indexes from schema metadata @@ -288,9 +326,11 @@ SQL-first exact-match workflow: --- ### 🧭 [20_graph_algorithms_route_planning.py](./20_graph_algorithms_route_planning.py) + **Graph Algorithms | shortestPath | dijkstra | astar | Route Comparison** SQL-first route-planning workflow: + - creates a directed multi-modal transport graph - compares minimum-hop routing against weighted routing - optimizes for distance, duration, and risk @@ -301,9 +341,11 @@ SQL-first route-planning workflow: --- ### 🕸️ [21_graph_analytical_view_sql.py](./21_graph_analytical_view_sql.py) + **Graph Analytical View | SQL DDL | Schema Metadata | Rebuild Lifecycle** SQL-first Graph Analytical View workflow: + - generates a larger synthetic transport graph with 100,000 base cities by default - creates a named GAV with vertex and edge property filters - polls `schema:graphAnalyticalViews` until the async build is `READY` @@ -316,9 +358,11 @@ SQL-first Graph Analytical View workflow: --- ### 🔢 [22_numpy_bulk_io.py](./22_numpy_bulk_io.py) + **numpy Bulk I/O | insert_many | append_samples | Columnar Export** Batched Python/Java boundary crossings for bulk workloads: + - Bulk document ingest with `Database.insert_many` (rows as one JSON batch), transactional and `parallel=True` - Time-series ingest straight from numpy arrays via `AsyncExecutor.append_samples` - Time-bucketed aggregation over the native `TIMESERIES` type @@ -329,9 +373,11 @@ Batched Python/Java boundary crossings for bulk workloads: --- ### 🌐 [23_server_mode_http_access.py](./23_server_mode_http_access.py) + **Server Mode | HTTP API | Bearer Auth | Mixed Access Pattern** Embedded-first server workflow, all in one process: + - starts ArcadeDB server mode from the Python package - reads server metadata over HTTP - creates a database through the server-managed Java API @@ -345,10 +391,12 @@ instead --- ### 🔁 [24_server_http_transactions_timeseries.py](./24_server_http_transactions_timeseries.py) + **Server HTTP API | Multi-Request Transactions | Database Commands | Line Protocol** Three server features that belong to the HTTP API, shown against the server that `create_server()` starts in this process: + - one transaction spanning several requests through the `arcadedb-session-id` header returned by `/api/v1/begin/{db}`, ended by `/commit` or `/rollback` - server-level database commands (`close database`, `open database`) sent to `/api/v1/server` @@ -362,9 +410,11 @@ HTTP only --- ### 🧮 [25_sparse_quantization_and_compact.py](./25_sparse_quantization_and_compact.py) + **Sparse Vectors | LSM_SPARSE_VECTOR | Weight Precision | COMPACT INDEX** Two decisions a sparse-retrieval workload should make on purpose: + - weight precision: `LSM_SPARSE_VECTOR` quantizes posting weights to INT8 by default; `"weightQuantization": "FP32"` keeps them exact - the settle step: `COMPACT INDEX` merges the LSM segments a bulk load leaves behind @@ -378,9 +428,11 @@ Two decisions a sparse-retrieval workload should make on purpose: --- ### ⚛️ [26_cross_model_transaction_atomicity.py](./26_cross_model_transaction_atomicity.py) + **Cross-Model Transaction | Vector + Graph + Document | Failure Injection** One operation that touches three models in one transaction: + - a vector search finds the nearest products, a graph hop expands to related products, and a document update bumps a counter on all of them - injects a failure between the writes N times, inside a transaction (rolled back, never @@ -394,9 +446,11 @@ half-applied after an interruption --- ### 🐼 [27_dataframe_arrow_ingest.py](./27_dataframe_arrow_ingest.py) + **DataFrame And Arrow Ingest | pandas | Polars | Arrow | Declared Types** DataFrames and Arrow tables loaded straight into a type, and a result back out as Polars: + - a pandas DataFrame into a document type (timestamps, nulls, a list column), in batches - a Polars DataFrame into a vertex type, then edges through `graph_batch().new_edges()` - an Arrow table into declared `INTEGER` and `DATETIME_MICROS` properties, and a float @@ -409,9 +463,11 @@ DataFrames and Arrow tables loaded straight into a type, and a result back out a Arrow type is stored as ### 🧩 [28_python_functions_and_record_events.py](./28_python_functions_and_record_events.py) + **Python Functions | Record Events | SQL + openCypher** Two things no query language can express: + - `db.register_function()` makes a Python `slugify` callable from SQL (SELECT and WHERE) and, as `py.slugify`, from openCypher; an exception in the function is a query error - measures the cost of one call on your machine against the same SQL expression @@ -425,9 +481,11 @@ errors, threads, and unregistering behave --- ### 🧠 [29_hybrid_memory_retrieval.py](./29_hybrid_memory_retrieval.py) + **Agent Memory | Tenant Pre-filter | Vector + Full-Text + Graph | `vector.fuse`** Hybrid retrieval for an agent memory in one SQL statement instead of three stores: + - `vector.neighbors` restricted to one tenant BEFORE the top-k (`where`, ArcadeDB 26.11.1, or `filter` with a RID subquery, 26.10.2 and later), a `SEARCH_INDEX` full-text match on the same tenant, both fused by reciprocal rank fusion with `vector.fuse`, then graph hops @@ -445,9 +503,11 @@ full-text and graph context in a single query --- ### 🔎 [30_columnar_facets.py](./30_columnar_facets.py) + **Faceted Queries | COLUMNAR Index | `FACETS` Clause | `result_set.facets`** The counts of a search sidebar next to the rows of a query: + - `CREATE INDEX ... COLUMNAR` on the properties that the facets count - one query with a `TERMS` facet (the 3 biggest categories) and a `RANGES` facet (price bands) next to a page of 5 rows, then `STATS` with `DISTINCT`, then a calendar `HISTOGRAM` @@ -469,7 +529,7 @@ rows, and how to read them from the result set as a dict - **Use directed graph assumptions** - Graph examples keep edge direction semantics. Every graph example stores both directions of each edge (the engine default). Load one-way edges only into an edge type declared one-way (`CREATE EDGE TYPE ... - UNIDIRECTIONAL`): a one-way load into a two-way type makes any query the planner walks +UNIDIRECTIONAL`): a one-way load into a two-way type makes any query the planner walks from the target end return 0 rows with no error (ArcadeData/arcadedb#8625) - **Database files persist** - Examples preserve data for inspection - **Output is educational** - Check console output to understand operations @@ -483,4 +543,4 @@ rows, and how to read them from the result set as a dict --- -*Examples are designed to be educational, and each includes detailed comments and step-by-step explanations. Most run on their own; 05 reads Example 04's database, 06 reads Example 05's, and 12 reuses Example 11's.* +_Examples are designed to be educational, and each includes detailed comments and step-by-step explanations. Most run on their own; 05 reads Example 04's database, 06 reads Example 05's, and 12 reuses Example 11's._ diff --git a/examples/download_data.py b/examples/download_data.py index 847538f..5e4afa2 100644 --- a/examples/download_data.py +++ b/examples/download_data.py @@ -2097,7 +2097,6 @@ def download_msmarco(count: int, data_dir: Path) -> Path: """Download MSMARCO v2.1 parquet parts and convert to shard files.""" size_label = f"{count // 1_000_000}M" out_dir = data_dir / f"MSMARCO-{size_label}" - meta_path = out_dir / f"msmarco-passages-{count}.meta.json" ensure_clean_dir(out_dir, f"MSMARCO {size_label}") diff --git a/native/wheel/README.md b/native/wheel/README.md index dd51cb9..ce528d8 100644 --- a/native/wheel/README.md +++ b/native/wheel/README.md @@ -5,6 +5,7 @@ This is `arcadedb`, the native build of the ArcadeDB Python bindings, and the de ArcadeDB (SQL, openCypher, document, graph, vector, time series) runs in your Python process **without a JVM and without JPype**. The engine is compiled ahead of time with GraalVM Community Edition into a shared library (`libarcadedb.so`, `.dylib` or `.dll`), and Python loads it with `ctypes`. It starts in milliseconds and has a small memory footprint. + > Release note: the first release of this layout is 26.11.1, and `arcadedb` has a final release on PyPI. A plain `uv add arcadedb` installs the final, and a later pre-release (`A.B.C.devN`) needs `uv add --prerelease allow arcadedb`. Report problems at and say that you use the native build. ## Install @@ -16,6 +17,7 @@ uv add arcadedb Optional extras for the columnar helpers: `[numpy]`, `[arrow]`, `[pandas]`, `[polars]`. + Old names: this package was formerly `arcadedb-embedded-native`, imported as `arcadedb_native`. The old project is archived on PyPI: it gets no new releases and no shim. The last release of the JVM project `arcadedb-embedded`, 26.11.1, is a shim that installs this package and warns once at import. If `arcadedb-jvm` is installed in the same environment, `import arcadedb` raises an `ImportError` that names both packages. Run `uv remove arcadedb arcadedb-jvm`, then install the one you want. @@ -47,12 +49,12 @@ with arcadedb.create_database("/tmp/demo") as db: CPython 3.11 to 3.15. One wheel per platform serves every Python version, because the library is loaded with `ctypes` and is not compiled against CPython. The images are built for the baseline CPU of the platform (`-march=compatibility`), so they do not need a special instruction set. On 3.15 the CI runs a subset of the tests, because jpype1 and ladybug, two dependencies of the full test suite, have no 3.15 wheel yet. -| Platform | Wheel tag | Note | -| --- | --- | --- | -| Linux x86-64, glibc 2.28+ | `manylinux_2_28_x86_64` | RHEL 8 and Rocky 8, Debian 10, Ubuntu 20.04 and newer | -| Linux arm64, glibc 2.28+ | `manylinux_2_28_aarch64` | | -| macOS arm64 | `macosx__arm64` | | -| Windows x64 | `win_amd64` | The newest wheel. It is built in CI, and its test run is the least proven of the four | +| Platform | Wheel tag | Note | +| ------------------------- | ------------------------ | ------------------------------------------------------------------------------------- | +| Linux x86-64, glibc 2.28+ | `manylinux_2_28_x86_64` | RHEL 8 and Rocky 8, Debian 10, Ubuntu 20.04 and newer | +| Linux arm64, glibc 2.28+ | `manylinux_2_28_aarch64` | | +| macOS arm64 | `macosx__arm64` | | +| Windows x64 | `win_amd64` | The newest wheel. It is built in CI, and its test run is the least proven of the four | Intel macOS, Windows arm64 and musl-based Linux (Alpine) are not built and not supported. On an Intel Mac use Docker or a Linux virtual machine; on Alpine use a glibc image such as `python:3.12-slim`. diff --git a/native/wheel/gen_notices.py b/native/wheel/gen_notices.py index d7a311d..89beef3 100644 --- a/native/wheel/gen_notices.py +++ b/native/wheel/gen_notices.py @@ -82,7 +82,7 @@ def sha(b): def text(b): # trailing whitespace is stripped per line (the repository's pre-commit hook does the same to the committed copy) s = b.decode("utf-8", "replace").replace("\r\n", "\n").replace("\r", "\n") - return "\n".join(l.rstrip() for l in s.split("\n")).rstrip() + "\n" + return "\n".join(ln.rstrip() for ln in s.split("\n")).rstrip() + "\n" def parse_classpath(build_log): @@ -103,8 +103,8 @@ def pom_licenses(pom, depth=0): root = ET.parse(pom).getroot() ns = {"m": root.tag.split("}")[0][1:]} names = [ - l.findtext("m:name", namespaces=ns) - for l in root.findall("m:licenses/m:license", ns) + ln.findtext("m:name", namespaces=ns) + for ln in root.findall("m:licenses/m:license", ns) ] if names: return names @@ -770,8 +770,8 @@ def main(): lineterm="", ) ) - for l in diff[:80]: - print(l[:300], file=sys.stderr) + for ln in diff[:80]: + print(ln[:300], file=sys.stderr) if len(diff) > 80: print(f"... {len(diff) - 80} more diff lines", file=sys.stderr) status = 1 @@ -786,14 +786,14 @@ def main(): f"run gen_notices.py ... --notice {a.notice} --notice-out {a.notice}", file=sys.stderr, ) - for l in difflib.unified_diff( + for ln in difflib.unified_diff( nhave.splitlines(), nnew.splitlines(), "committed", "generated", lineterm="", ): - print(l[:400], file=sys.stderr) + print(ln[:400], file=sys.stderr) status = 1 for what, why in review: print(f"licence review: {what}: {why}", file=sys.stderr) diff --git a/pyproject.toml b/pyproject.toml index 69caffe..c029dfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,3 +178,20 @@ exclude_dirs = ["/tests/.cache/", "/build/", "/dist/"] # (src/) intentionally contains no assert statements - so we skip it globally # rather than maintaining a per-file glob list. skips = ["B101"] + +[tool.ruff] +# Black's default; no hard limit on comments and strings (E501 is intentionally not enabled, so +# the ~2,904 lines Black deliberately leaves long are not flagged). See issue #270. +line-length = 88 +target-version = "py311" + +[tool.ruff.lint] +# Ruff's default rules: E4 (imports), E7 (statements), E9 (runtime), F (pyflakes). +select = ["E4", "E7", "E9", "F"] + +[tool.ruff.lint.per-file-ignores] +# pytest fixtures whose names shadow the fixture are a normal pattern (issue #270 lists 8 such +# F811s). The one real duplicate (TestInsertManyVertexType in tests/test_bulk_insert.py) is removed +# pytest fixtures whose names shadow the fixture are a normal pattern (issue #270 lists 8 such +# F811s). The duplicate TestInsertManyVertexType in tests/test_bulk_insert.py has been removed. +"tests/**" = ["F811"] diff --git a/scripts/run_examples.sh b/scripts/run_examples.sh index fdf43fd..df82014 100755 --- a/scripts/run_examples.sh +++ b/scripts/run_examples.sh @@ -105,7 +105,7 @@ run_examples() { # Create results file results_file="example-results.txt" - > $results_file + true > "$results_file" # Select example files by pattern(s) in EXAMPLES (space-separated globs) examples=$(ls $EXAMPLES 2> /dev/null | sort) diff --git a/scripts/smoke_wheel.py b/scripts/smoke_wheel.py index e53249b..3f3957b 100644 --- a/scripts/smoke_wheel.py +++ b/scripts/smoke_wheel.py @@ -9,7 +9,9 @@ import arcadedb -print(f"arcadedb {arcadedb.__version__}, backend {arcadedb.__backend__}, engine {arcadedb.ENGINE_VERSION}") +print( + f"arcadedb {arcadedb.__version__}, backend {arcadedb.__backend__}, engine {arcadedb.ENGINE_VERSION}" +) assert arcadedb.__backend__ == "native", arcadedb.__backend__ with tempfile.TemporaryDirectory() as d: with arcadedb.create_database(os.path.join(d, "smoke")) as db: @@ -17,5 +19,10 @@ with db.transaction(): db.command("sql", "CREATE VERTEX TYPE P") db.command("sql", "CREATE VERTEX P SET name = 'Ada'") - assert db.query("opencypher", "MATCH (n:P) RETURN n.name AS name").to_list()[0]["name"] == "Ada" + assert ( + db.query("opencypher", "MATCH (n:P) RETURN n.name AS name").to_list()[0][ + "name" + ] + == "Ada" + ) print("smoke ok") diff --git a/scripts/verify_wheel_tag.py b/scripts/verify_wheel_tag.py index 7242129..6eac045 100644 --- a/scripts/verify_wheel_tag.py +++ b/scripts/verify_wheel_tag.py @@ -32,26 +32,42 @@ def problems(wheel: Path) -> list[str]: # name-version(-build)-python-abi-platform.whl parts = wheel.name[: -len(".whl")].split("-") if len(parts) not in (5, 6): - return [f"{wheel.name}: not a wheel file name (name-version-python-abi-platform.whl)"] + return [ + f"{wheel.name}: not a wheel file name (name-version-python-abi-platform.whl)" + ] python, abi, platform = parts[-3:] if (python, abi) != ("py3", "none"): found.append(f"{wheel.name}: tagged {python}-{abi}, expected py3-none") if platform == "any" or not platform: - found.append(f"{wheel.name}: platform tag {platform!r}; the wheel bundles a platform-specific JRE") + found.append( + f"{wheel.name}: platform tag {platform!r}; the wheel bundles a platform-specific JRE" + ) tag = f"{python}-{abi}-{platform}" with zipfile.ZipFile(wheel) as zf: names = [n for n in zf.namelist() if n.endswith(".dist-info/WHEEL")] if len(names) != 1: - return found + [f"{wheel.name}: expected one .dist-info/WHEEL, found {names}"] + return found + [ + f"{wheel.name}: expected one .dist-info/WHEEL, found {names}" + ] # strip: a wheel built on Windows has CRLF line endings - lines = [line.strip() for line in zf.read(names[0]).decode("utf-8").splitlines()] + lines = [ + line.strip() for line in zf.read(names[0]).decode("utf-8").splitlines() + ] tags = [line.split(":", 1)[1].strip() for line in lines if line.startswith("Tag:")] if tags != [tag]: - found.append(f"{wheel.name}: the WHEEL file says Tag {tags}, the file name says {tag}") - purelib = [line.split(":", 1)[1].strip().lower() for line in lines if line.startswith("Root-Is-Purelib:")] + found.append( + f"{wheel.name}: the WHEEL file says Tag {tags}, the file name says {tag}" + ) + purelib = [ + line.split(":", 1)[1].strip().lower() + for line in lines + if line.startswith("Root-Is-Purelib:") + ] if purelib != ["false"]: - found.append(f"{wheel.name}: Root-Is-Purelib is {purelib}, expected false (a platform wheel)") + found.append( + f"{wheel.name}: Root-Is-Purelib is {purelib}, expected false (a platform wheel)" + ) return found @@ -72,7 +88,9 @@ def main(argv: list[str] | None = None) -> int: for line in found: print(f"ERROR: {line}", file=sys.stderr) else: - print(f"OK: {wheel.name} is tagged {'-'.join(wheel.name[: -len('.whl')].split('-')[-3:])}") + print( + f"OK: {wheel.name} is tagged {'-'.join(wheel.name[: -len('.whl')].split('-')[-3:])}" + ) if status: print( " The wheel must be py3-none-: see setup.py (bdist_wheel.get_tag). Do not publish it.", diff --git a/src/arcadedb/__init__.py b/src/arcadedb/__init__.py index d208741..86e1208 100644 --- a/src/arcadedb/__init__.py +++ b/src/arcadedb/__init__.py @@ -39,13 +39,13 @@ __version__ = "0.0.0" # Import async execution -from .async_executor import AsyncExecutor +from .async_executor import AsyncExecutor # noqa: E402 # Import record event handle -from .callbacks import EventHandle +from .callbacks import EventHandle # noqa: E402 # Import core database classes -from .core import ( +from .core import ( # noqa: E402 Database, DatabaseFactory, create_database, @@ -54,41 +54,44 @@ ) # Import exceptions -from .exceptions import ArcadeDBError +from .exceptions import ArcadeDBError # noqa: E402 # Import exporter classes -from .exporter import export_database, export_to_csv +from .exporter import export_database, export_to_csv # noqa: E402 # Import graph classes -from .graph import Document, Edge, Vertex +from .graph import Document, Edge, Vertex # noqa: E402 # Import graph batch helper -from .graph_batch import GraphBatch +from .graph_batch import GraphBatch # noqa: E402 # Import importer helpers -from .importer import ImportResult +from .importer import ImportResult # noqa: E402 # Which engine this install actually carries. __version__ is the PACKAGE # version and can disagree with the bundled JARs without any error. -from .jvm import jar_fingerprint +from .jvm import jar_fingerprint # noqa: E402 # Import result classes -from .results import Result, ResultSet +from .results import Result, ResultSet # noqa: E402 # Import schema classes -from .schema import IndexType, PropertyType, Schema +from .schema import IndexType, PropertyType, Schema # noqa: E402 # Import server classes -from .server import ArcadeDBServer, create_server +from .server import ArcadeDBServer, create_server # noqa: E402 # Import transaction management -from .transactions import TransactionContext +from .transactions import TransactionContext # noqa: E402 # Import type conversion utilities -from .type_conversion import convert_java_to_python, convert_python_to_java +from .type_conversion import ( # noqa: E402 + convert_java_to_python, + convert_python_to_java, +) # Import vector utilities and index -from .vector import ( +from .vector import ( # noqa: E402 VectorIndex, to_java_byte_array, to_java_float_array, diff --git a/src/arcadedb/_native/extras.py b/src/arcadedb/_native/extras.py index a0aa517..1b106ed 100644 --- a/src/arcadedb/_native/extras.py +++ b/src/arcadedb/_native/extras.py @@ -13,16 +13,15 @@ import threading import weakref from collections.abc import Mapping -from typing import Any, Callable, Iterable, Optional +from typing import Callable, Optional from ..callbacks import EventHandle as _BaseEventHandle from ..callbacks import check_event, check_function from ..exceptions import ArcadeDBError from ..graph_batch import GraphBatch as _BaseGraphBatch from ..type_conversion import take_vector_columns -from . import interrupts, lib, not_yet +from . import interrupts, lib from .codec import ( - _PLAIN, _default, _np, _prep, diff --git a/src/arcadedb/_native/lib.py b/src/arcadedb/_native/lib.py index 600f381..aaceedc 100644 --- a/src/arcadedb/_native/lib.py +++ b/src/arcadedb/_native/lib.py @@ -219,30 +219,32 @@ class _Params(ctypes.Structure): lib.graal_get_current_thread.restype = ctypes.c_void_p lib.graal_detach_thread.argtypes = [ctypes.c_void_p] lib.graal_detach_thread.restype = ctypes.c_int - # a Java long is 64 bits on every platform; C long is 32 bits on Windows, so L is long long (the same size as c_long elsewhere) - P, L, I, S = ctypes.c_void_p, ctypes.c_longlong, ctypes.c_int, ctypes.c_char_p + # One-letter ctypes aliases used in the sigs table below: + # P = c_void_p (pointer), L = c_longlong (Java long, 64 bits everywhere; + # C long is 32 bits on Windows), N = c_int (integer), S = c_char_p (string). + P, L, N, S = ctypes.c_void_p, ctypes.c_longlong, ctypes.c_int, ctypes.c_char_p sigs = { - "arcadedb_n_open": [P, S, I], - "arcadedb_n_close": [P, L, I], + "arcadedb_n_open": [P, S, N], + "arcadedb_n_close": [P, L, N], "arcadedb_n_info": [P, L], - "arcadedb_n_tx": [P, L, I], - "arcadedb_n_exec": [P, L, I, S, S, S, I], - "arcadedb_n_exec_bin": [P, L, I, S, S, S, I, S, L], + "arcadedb_n_tx": [P, L, N], + "arcadedb_n_exec": [P, L, N, S, S, S, N], + "arcadedb_n_exec_bin": [P, L, N, S, S, S, N, S, L], "arcadedb_n_insert_cols": [P, L, S, S, S, L], - "arcadedb_n_insert_colp": [P, L, S, S, P, I], - "arcadedb_n_next": [P, L, I], - "arcadedb_n_next_bin": [P, L, I], - "arcadedb_n_next_col": [P, L, I, S, I], + "arcadedb_n_insert_colp": [P, L, S, S, P, N], + "arcadedb_n_next": [P, L, N], + "arcadedb_n_next_bin": [P, L, N], + "arcadedb_n_next_col": [P, L, N, S, N], "arcadedb_n_rs_close": [P, L], "arcadedb_n_facets": [P, L], - "arcadedb_n_cmd": [P, L, I, S, S, S, S, L, I], + "arcadedb_n_cmd": [P, L, N, S, S, S, S, L, N], "arcadedb_n_last_error": [P], - "arcadedb_n_rs_close_many": [P, P, I], + "arcadedb_n_rs_close_many": [P, P, N], "arcadedb_n_exec_many": [P, L, S, S, S], "arcadedb_n_insert_rows": [P, L, S, S], "arcadedb_n_server_start": [P, S], "arcadedb_n_server_stop": [P, L], - "arcadedb_n_server_db": [P, L, S, I], + "arcadedb_n_server_db": [P, L, S, N], "arcadedb_n_diag": [P], "arcadedb_n_sample_dump": [P], "arcadedb_n_dbset": [P, L, S, S], @@ -251,10 +253,10 @@ class _Params(ctypes.Structure): "arcadedb_n_gb": [P, L, S, S], "arcadedb_n_gb_bin": [P, L, S, S, S, L], "arcadedb_n_async": [P, L, S, S], - "arcadedb_n_async_bin": [P, L, S, S, P, I], + "arcadedb_n_async_bin": [P, L, S, S, P, N], "arcadedb_n_vindex": [P, L, S, S], "arcadedb_n_import": [P, L, S], - "arcadedb_n_export": [P, L, S, S, I, S], + "arcadedb_n_export": [P, L, S, S, N, S], "arcadedb_n_cb": [P, S, S], "arcadedb_n_alloc": [P, L], "arcadedb_n_set_upcall": [P, P], diff --git a/src/arcadedb/_native/server.py b/src/arcadedb/_native/server.py index 090239c..f8193fe 100644 --- a/src/arcadedb/_native/server.py +++ b/src/arcadedb/_native/server.py @@ -1,6 +1,5 @@ """In-process server on the native backend (HTTP API, wire-protocol plugins as configured; no Studio in the native wheel).""" -import os from typing import Any, Dict, Optional from ..exceptions import ArcadeDBError diff --git a/src/arcadedb/core.py b/src/arcadedb/core.py index 792d324..332fd1b 100644 --- a/src/arcadedb/core.py +++ b/src/arcadedb/core.py @@ -11,7 +11,10 @@ from datetime import date from decimal import Decimal from os import PathLike -from typing import Any, Callable, List, Optional +from typing import TYPE_CHECKING, Any, Callable, List, Optional + +if TYPE_CHECKING: + from .vector import VectorIndex import jpype @@ -37,7 +40,6 @@ bulk_row_ok, convert_python_to_java, declared_float_vectors, - json_bulk_dumps, json_bulk_dumps_tagged, split_float_vectors, ) diff --git a/src/arcadedb/jvm.py b/src/arcadedb/jvm.py index f38067a..02aa7b8 100644 --- a/src/arcadedb/jvm.py +++ b/src/arcadedb/jvm.py @@ -21,11 +21,9 @@ import jpype.imports from . import aot as _aot -from ._logdir import LOG_DIR_ENV as _LOG_DIR_ENV from ._logdir import console_enabled as _console_enabled from ._logdir import default_log_dir as _default_log_dir from ._logdir import logging_config_name as _logging_config_name -from ._logdir import render_logging_config as _render_logging_config from ._logdir import stdio_log_path as _stdio_log_path from ._logdir import usable_log_dir as _usable_log_dir from ._logdir import write_logging_config as _write_logging_config diff --git a/tests/test_async_transaction.py b/tests/test_async_transaction.py index 94c70cf..e98bf4c 100644 --- a/tests/test_async_transaction.py +++ b/tests/test_async_transaction.py @@ -4,8 +4,6 @@ import pytest -import arcadedb - TIMEOUT = 30 diff --git a/tests/test_bulk_insert.py b/tests/test_bulk_insert.py index 444a185..2703d01 100644 --- a/tests/test_bulk_insert.py +++ b/tests/test_bulk_insert.py @@ -246,104 +246,6 @@ def test_a_missing_type_still_raises(self, temp_db): temp_db.insert_many("Nope", [{"id": 1}]) -class TestInsertManyVertexType: - """insert_many and insert_columns into a VERTEX type store real vertices. - - The JVM wheel raised "Cannot create a document of type ... because is not a - document type" (the Java helper called newDocument), while the native wheel - accepted the same call (found 2026-10-25 by a parity probe between the two - wheels). A record counts as a vertex when an edge can join it: CREATE EDGE - refuses a record that is not one. - """ - - ROWS = 25 - - def _vertex_db(self, db, name="Vtx", buckets=None): - suffix = f" BUCKETS {buckets}" if buckets else "" - db.command("sql", f"CREATE VERTEX TYPE {name}{suffix}") - db.command("sql", "CREATE EDGE TYPE Knows") - - def _assert_vertices(self, db, name, expected): - assert _count(db, name) == expected - with db.transaction(): - db.command( - "sql", - f"CREATE EDGE Knows FROM (SELECT FROM {name} WHERE id = 0) " # nosec B608 - test-owned type name, no user input - f"TO (SELECT FROM {name} WHERE id = 1)", - ) - assert _count(db, "Knows") == 1 - out = db.query( - "sql", - f"SELECT out('Knows').size() AS n FROM {name} WHERE id = 0", # nosec B608 - test-owned type name, no user input - ).to_list()[0]["n"] - assert out == 1 - - def test_insert_many(self, temp_db): - self._vertex_db(temp_db) - rows = [{"id": i, "name": f"v{i}", "tags": ["a"]} for i in range(self.ROWS)] - assert temp_db.insert_many("Vtx", rows, commit_every=10) == self.ROWS - self._assert_vertices(temp_db, "Vtx", self.ROWS) - got = temp_db.query("sql", "SELECT FROM Vtx WHERE id = 7").to_list()[0] - assert got["name"] == "v7" - assert list(got["tags"]) == ["a"] - - def test_insert_many_parallel(self, temp_db): - self._vertex_db(temp_db, buckets=2) - rows = [{"id": i} for i in range(2000)] - assert temp_db.insert_many("Vtx", rows, parallel=True) == 2000 - self._assert_vertices(temp_db, "Vtx", 2000) - - def test_insert_many_non_json_fallback(self, temp_db): - self._vertex_db(temp_db) - rows = [ - {"id": i, "when": datetime.datetime(2026, 7, 25, 12, 0, i)} - for i in range(3) - ] - assert temp_db.insert_many("Vtx", rows) == 3 - self._assert_vertices(temp_db, "Vtx", 3) - - def test_insert_many_inside_a_caller_transaction(self, temp_db): - self._vertex_db(temp_db) - with pytest.raises(RuntimeError): - with temp_db.transaction(): - temp_db.insert_many("Vtx", [{"id": 0}, {"id": 1}], commit_every=1) - raise RuntimeError("roll back") - assert _count(temp_db, "Vtx") == 0 - with temp_db.transaction(): - temp_db.insert_many("Vtx", [{"id": 0}, {"id": 1}], commit_every=1) - self._assert_vertices(temp_db, "Vtx", 2) - - def test_insert_columns(self, temp_db): - import numpy as np - - self._vertex_db(temp_db) - assert temp_db.insert_columns("Vtx", {"id": np.arange(5, dtype="int64")}) == 5 - self._assert_vertices(temp_db, "Vtx", 5) - - def test_insert_columns_parallel(self, temp_db): - import numpy as np - - self._vertex_db(temp_db, buckets=2) - n = 1000 - assert ( - temp_db.insert_columns( - "Vtx", {"id": np.arange(n, dtype="int64")}, parallel=True - ) - == n - ) - self._assert_vertices(temp_db, "Vtx", n) - - def test_an_edge_type_is_refused(self, temp_db): - self._vertex_db(temp_db) - with pytest.raises(arcadedb.ArcadeDBError, match="Knows"): - temp_db.insert_many("Knows", [{"id": 1}]) - assert _count(temp_db, "Knows") == 0 - - def test_a_missing_type_still_raises(self, temp_db): - with pytest.raises(arcadedb.ArcadeDBError, match="Nope"): - temp_db.insert_many("Nope", [{"id": 1}]) - - class TestInsertManyParallelReportsFailures: """A record the parallel writers fail to store must fail the call. @@ -801,7 +703,7 @@ class TestVectorColumnsDataFrame: def test_vector_column_to_dataframe(self, temp_db): import arcadedb - pd = __import__("pytest").importorskip("pandas") + pytest.importorskip("pandas") temp_db.command("sql", "CREATE DOCUMENT TYPE EmbDf") temp_db.command("sql", "CREATE PROPERTY EmbDf.v ARRAY_OF_FLOATS") with temp_db.transaction(): diff --git a/tests/test_core.py b/tests/test_core.py index 0076bd4..e110c7f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3,8 +3,6 @@ These tests work with our base package (includes SQL, OpenCypher, Studio). """ -import json - import pytest import arcadedb diff --git a/tests/test_float_vector_bulk.py b/tests/test_float_vector_bulk.py index d0a5a00..32aca80 100644 --- a/tests/test_float_vector_bulk.py +++ b/tests/test_float_vector_bulk.py @@ -13,7 +13,6 @@ import numpy as np import pytest -import arcadedb from arcadedb import type_conversion DIM = 8 diff --git a/tests/test_free_threading.py b/tests/test_free_threading.py index ad7a130..4bc1cd4 100644 --- a/tests/test_free_threading.py +++ b/tests/test_free_threading.py @@ -347,7 +347,10 @@ def test_threads(scenario, tmp): if __name__ == "__main__": - gil = lambda: getattr(sys, "_is_gil_enabled", lambda: True)() + + def gil() -> bool: + return getattr(sys, "_is_gil_enabled", lambda: True)() + print( f"FTSTRESS python={sys.version.split()[0]} gil={gil()} backend={'native' if NATIVE else 'jpype'} " f"threads={THREADS} repeat={REPEAT} rows={ROWS}", diff --git a/tests/test_graph_api.py b/tests/test_graph_api.py index 235b31f..2f6a8bf 100644 --- a/tests/test_graph_api.py +++ b/tests/test_graph_api.py @@ -4,8 +4,9 @@ Tests Document, Vertex, and Edge wrapper classes and their methods. """ -import arcadedb import pytest + +import arcadedb from arcadedb import ArcadeDBError from arcadedb.graph import Document, Edge, Vertex diff --git a/tests/test_jar_closure.py b/tests/test_jar_closure.py index 144d697..a1d63b1 100644 --- a/tests/test_jar_closure.py +++ b/tests/test_jar_closure.py @@ -37,8 +37,6 @@ class refers to a library the wheel does not ship: decide whether the code path import struct import zipfile -import pytest - from arcadedb.jvm import get_jar_path # Packages the JDK provides. The first test checks the jars only. The bundled JRE diff --git a/tests/test_log_dir.py b/tests/test_log_dir.py index 337c1b2..98d7816 100644 --- a/tests/test_log_dir.py +++ b/tests/test_log_dir.py @@ -10,9 +10,8 @@ from pathlib import Path from unittest.mock import patch -import pytest - from arcadedb import _logdir, jvm +from arcadedb._logdir import render_logging_config as _render_logging_config from arcadedb._native import lib as native_lib from arcadedb.jvm import _build_jvm_args, _default_log_dir @@ -73,7 +72,7 @@ def _run_child(tmp_path, env_overrides, jvm_args=()): finally: cwd.chmod(0o755) assert result.returncode == 0, result.stdout + result.stderr - line = next(l for l in result.stdout.splitlines() if l.startswith("LOGDIR=")) + line = next(ln for ln in result.stdout.splitlines() if ln.startswith("LOGDIR=")) return cwd, Path(line[len("LOGDIR=") :]), home @@ -177,7 +176,7 @@ def test_rendered_config_keeps_the_engine_settings_and_moves_the_file_log(): "java.util.logging.FileHandler.pattern=./log/arcadedb.log\n" "java.util.logging.FileHandler.count=10\n" ) - rendered = jvm._render_logging_config(engine) + rendered = _render_logging_config(engine) assert "./log" not in rendered assert "FileHandler.count=10" in rendered assert rendered.count("FileHandler.pattern") == 1 diff --git a/tests/test_native_statement_texts.py b/tests/test_native_statement_texts.py index cfe42d5..c3dbbec 100644 --- a/tests/test_native_statement_texts.py +++ b/tests/test_native_statement_texts.py @@ -8,8 +8,9 @@ import re -import arcadedb as arcadedb import pytest + +import arcadedb as arcadedb from arcadedb._native.database import NativeDatabase RID_IN_TEXT = re.compile(r"#-?\d+:-?\d+") diff --git a/tests/test_native_udf.py b/tests/test_native_udf.py index 234794a..54173f9 100644 --- a/tests/test_native_udf.py +++ b/tests/test_native_udf.py @@ -4,7 +4,6 @@ import pytest -import arcadedb from arcadedb import ArcadeDBError diff --git a/tests/test_percall_calls.py b/tests/test_percall_calls.py index 80cc132..c9b8ab0 100644 --- a/tests/test_percall_calls.py +++ b/tests/test_percall_calls.py @@ -7,7 +7,6 @@ choice, the same errors, and the same result-set state after ``first()``. """ -import jpype import numpy as np import pytest diff --git a/tests/test_python_function_interrupt.py b/tests/test_python_function_interrupt.py index 37fa818..eeddd75 100644 --- a/tests/test_python_function_interrupt.py +++ b/tests/test_python_function_interrupt.py @@ -151,12 +151,14 @@ def test_sigint_loop_in_a_fresh_process_ends_cleanly(): """The same loop in its own process. A Ctrl-C raised as the Python callback is entered used to leave the engine a stale reply pointer, and the process aborted in ``free()`` (nonzero exit); pytest's own hooks hid it inside a test run. """ - result = subprocess.run( # nosec B603 - fixed argv, no shell, test-owned code string - [sys.executable, "-c", _SIGINT_LOOP], - capture_output=True, - text=True, - timeout=600, - check=False, + result = ( + subprocess.run( # nosec B603 - fixed argv, no shell, test-owned code string + [sys.executable, "-c", _SIGINT_LOOP], + capture_output=True, + text=True, + timeout=600, + check=False, + ) ) assert result.returncode == 0, result.stderr[-2000:] assert int(result.stdout.split()[-1]) >= 1, result.stdout diff --git a/tests/test_release_name_guards.py b/tests/test_release_name_guards.py index 9f69c35..e8651be 100644 --- a/tests/test_release_name_guards.py +++ b/tests/test_release_name_guards.py @@ -8,7 +8,6 @@ from __future__ import annotations import os -import re import subprocess # nosec B404 - test-controlled child processes, no untrusted input import sys import textwrap @@ -39,7 +38,9 @@ def _script(workflow: str, step: str) -> str: """The python3 heredoc of the first step called `step` in the workflow file, dedented.""" lines = (WORKFLOWS / workflow).read_text(encoding="utf-8").splitlines() start = next(i for i, line in enumerate(lines) if line.strip().startswith(step)) - begin = next(i for i in range(start, len(lines)) if "python3 -I - <<'PY'" in lines[i]) + begin = next( + i for i in range(start, len(lines)) if "python3 -I - <<'PY'" in lines[i] + ) end = next(i for i in range(begin + 1, len(lines)) if lines[i].strip() == "PY") return textwrap.dedent("\n".join(lines[begin + 1 : end])) + "\n" @@ -55,7 +56,13 @@ def _wheel( dist = file_name.split("-")[0] path = directory / f"{file_name}.whl" meta = newline.join( - ["Metadata-Version: 2.4", f"Name: {meta_name}", f"Version: {TAG}", "Summary: x", ""] + [ + "Metadata-Version: 2.4", + f"Name: {meta_name}", + f"Version: {TAG}", + "Summary: x", + "", + ] ) with zipfile.ZipFile(path, "w") as zf: zf.writestr(f"{dist}-{TAG}.dist-info/METADATA", meta) @@ -63,7 +70,14 @@ def _wheel( wheel_tag = "-".join(file_name.split("-")[-3:]) zf.writestr( f"{dist}-{TAG}.dist-info/WHEEL", - newline.join(["Wheel-Version: 1.0", "Root-Is-Purelib: false", f"Tag: {wheel_tag}", ""]), + newline.join( + [ + "Wheel-Version: 1.0", + "Root-Is-Purelib: false", + f"Tag: {wheel_tag}", + "", + ] + ), ) if top_level is not None: zf.writestr(f"{dist}-{TAG}.dist-info/top_level.txt", top_level + newline) @@ -82,7 +96,9 @@ def _run(script: str, cwd: Path, **env: str) -> subprocess.CompletedProcess: ) -def _guard(workflow: str, dist: str, tmp_path: Path, wheels) -> subprocess.CompletedProcess: +def _guard( + workflow: str, dist: str, tmp_path: Path, wheels +) -> subprocess.CompletedProcess: (tmp_path / "dist").mkdir() for file_name, meta_name, newline in wheels: _wheel(tmp_path / "dist", file_name, meta_name, newline) @@ -95,7 +111,9 @@ def _guard(workflow: str, dist: str, tmp_path: Path, wheels) -> subprocess.Compl [("\n", ""), ("\r\n", ""), ("\n", " "), ("\r\n", " \t")], ids=["lf", "crlf", "trailing-spaces", "crlf-and-tab"], ) -def test_the_right_distribution_passes_whatever_ends_the_line(workflow, dist, newline, suffix, tmp_path): +def test_the_right_distribution_passes_whatever_ends_the_line( + workflow, dist, newline, suffix, tmp_path +): file_name = f"{dist.replace('-', '_')}-{TAG}-py3-none-any" result = _guard(workflow, dist, tmp_path, [(file_name, dist + suffix, newline)]) assert result.returncode == 0, result.stdout + result.stderr @@ -121,7 +139,12 @@ def test_another_name_in_the_metadata_is_refused(workflow, dist, newline, tmp_pa @pytest.mark.parametrize("workflow, dist", PUBLISHERS) def test_another_name_in_the_file_name_is_refused(workflow, dist, tmp_path): - result = _guard(workflow, dist, tmp_path, [(f"{OTHER_FILE_STEM}-{TAG}-py3-none-any", dist, "\r\n")]) + result = _guard( + workflow, + dist, + tmp_path, + [(f"{OTHER_FILE_STEM}-{TAG}-py3-none-any", dist, "\r\n")], + ) assert result.returncode != 0 assert "Refusing to publish" in result.stdout + result.stderr @@ -144,8 +167,18 @@ def test_the_native_workflow_refuses_a_jvm_wheel_and_the_other_way_round(tmp_pat native.mkdir() jvm = tmp_path / "jvm" jvm.mkdir() - as_jvm = _guard("release-native.yml", "arcadedb", native, [(f"arcadedb_jvm-{TAG}-py3-none-any", "arcadedb-jvm", "\n")]) - as_native = _guard("release-jvm.yml", "arcadedb-jvm", jvm, [(f"arcadedb-{TAG}-py3-none-any", "arcadedb", "\n")]) + as_jvm = _guard( + "release-native.yml", + "arcadedb", + native, + [(f"arcadedb_jvm-{TAG}-py3-none-any", "arcadedb-jvm", "\n")], + ) + as_native = _guard( + "release-jvm.yml", + "arcadedb-jvm", + jvm, + [(f"arcadedb-{TAG}-py3-none-any", "arcadedb", "\n")], + ) assert as_jvm.returncode != 0 assert as_native.returncode != 0 @@ -153,8 +186,12 @@ def test_the_native_workflow_refuses_a_jvm_wheel_and_the_other_way_round(tmp_pat def _jvm_release_tree(root: Path, windows_metadata_name: str | None = None) -> Path: """What the JVM "Verify wheel count, versions and sizes" step reads: a size script, the real tag script and 4 wheels (one for each platform).""" (root / "scripts").mkdir() - (root / "scripts" / "verify_wheel_size.py").write_text("import sys\nsys.exit(0)\n", encoding="utf-8") - (root / "scripts" / "verify_wheel_tag.py").write_bytes((REPO / "scripts" / "verify_wheel_tag.py").read_bytes()) + (root / "scripts" / "verify_wheel_size.py").write_text( + "import sys\nsys.exit(0)\n", encoding="utf-8" + ) + (root / "scripts" / "verify_wheel_tag.py").write_bytes( + (REPO / "scripts" / "verify_wheel_tag.py").read_bytes() + ) dist = root / "dist" dist.mkdir() platforms = { @@ -164,15 +201,30 @@ def _jvm_release_tree(root: Path, windows_metadata_name: str | None = None) -> P "win_amd64": "\r\n", # built on Windows: CRLF, as in 26.11.1.dev0 } for platform, newline in platforms.items(): - name = windows_metadata_name if (platform == "win_amd64" and windows_metadata_name) else "arcadedb-jvm" - _wheel(dist, f"arcadedb_jvm-{TAG}-py3-none-{platform}", name, newline, top_level="arcadedb") + name = ( + windows_metadata_name + if (platform == "win_amd64" and windows_metadata_name) + else "arcadedb-jvm" + ) + _wheel( + dist, + f"arcadedb_jvm-{TAG}-py3-none-{platform}", + name, + newline, + top_level="arcadedb", + ) return root def test_the_jvm_wheel_check_accepts_crlf_metadata_from_the_windows_wheels(tmp_path): root = _jvm_release_tree(tmp_path) output = tmp_path / "github_output" - result = _run(_script("release-jvm.yml", VERIFY_STEP), root, TAG=TAG, GITHUB_OUTPUT=str(output)) + result = _run( + _script("release-jvm.yml", VERIFY_STEP), + root, + TAG=TAG, + GITHUB_OUTPUT=str(output), + ) assert result.returncode == 0, result.stdout + result.stderr assert "distribution arcadedb-jvm, import package arcadedb" in result.stdout assert "import-name=arcadedb" in output.read_text(encoding="utf-8") @@ -181,7 +233,12 @@ def test_the_jvm_wheel_check_accepts_crlf_metadata_from_the_windows_wheels(tmp_p def test_the_jvm_wheel_check_still_refuses_wheels_of_two_distributions(tmp_path): root = _jvm_release_tree(tmp_path, windows_metadata_name=OTHER_DIST) output = tmp_path / "github_output" - result = _run(_script("release-jvm.yml", VERIFY_STEP), root, TAG=TAG, GITHUB_OUTPUT=str(output)) + result = _run( + _script("release-jvm.yml", VERIFY_STEP), + root, + TAG=TAG, + GITHUB_OUTPUT=str(output), + ) assert result.returncode != 0 assert "disagree on distribution name" in result.stdout @@ -189,15 +246,30 @@ def test_the_jvm_wheel_check_still_refuses_wheels_of_two_distributions(tmp_path) def _jvm_release_result(tmp_path: Path, mutate) -> subprocess.CompletedProcess: root = _jvm_release_tree(tmp_path) mutate(root / "dist") - return _run(_script("release-jvm.yml", VERIFY_STEP), root, TAG=TAG, GITHUB_OUTPUT=str(tmp_path / "github_output")) + return _run( + _script("release-jvm.yml", VERIFY_STEP), + root, + TAG=TAG, + GITHUB_OUTPUT=str(tmp_path / "github_output"), + ) def test_the_jvm_wheel_check_wants_one_wheel_for_each_platform_and_no_more(tmp_path): # the layout of 26.11.1.dev0 and before: one cpXY wheel for each Python and platform, 16 files def per_python(dist: Path) -> None: for py in ("cp311", "cp312"): - for platform in ("manylinux_2_28_x86_64", "manylinux_2_28_aarch64", "macosx_11_0_arm64", "win_amd64"): - _wheel(dist, f"arcadedb_jvm-{TAG}-{py}-{py}-{platform}", "arcadedb-jvm", top_level="arcadedb") + for platform in ( + "manylinux_2_28_x86_64", + "manylinux_2_28_aarch64", + "macosx_11_0_arm64", + "win_amd64", + ): + _wheel( + dist, + f"arcadedb_jvm-{TAG}-{py}-{py}-{platform}", + "arcadedb-jvm", + top_level="arcadedb", + ) result = _jvm_release_result(tmp_path, per_python) assert result.returncode != 0 @@ -207,7 +279,13 @@ def per_python(dist: Path) -> None: def test_the_jvm_wheel_check_refuses_a_platform_wheel_tagged_for_one_python(tmp_path): def retag(dist: Path) -> None: (dist / f"arcadedb_jvm-{TAG}-py3-none-win_amd64.whl").unlink() - _wheel(dist, f"arcadedb_jvm-{TAG}-cp312-cp312-win_amd64", "arcadedb-jvm", "\r\n", top_level="arcadedb") + _wheel( + dist, + f"arcadedb_jvm-{TAG}-cp312-cp312-win_amd64", + "arcadedb-jvm", + "\r\n", + top_level="arcadedb", + ) result = _jvm_release_result(tmp_path, retag) assert result.returncode != 0 @@ -217,7 +295,12 @@ def retag(dist: Path) -> None: def test_the_jvm_wheel_check_refuses_a_wheel_for_no_platform(tmp_path): def drop_a_platform(dist: Path) -> None: (dist / f"arcadedb_jvm-{TAG}-py3-none-macosx_11_0_arm64.whl").unlink() - _wheel(dist, f"arcadedb_jvm-{TAG}-py3-none-any", "arcadedb-jvm", top_level="arcadedb") + _wheel( + dist, + f"arcadedb_jvm-{TAG}-py3-none-any", + "arcadedb-jvm", + top_level="arcadedb", + ) result = _jvm_release_result(tmp_path, drop_a_platform) assert result.returncode != 0 diff --git a/tests/test_server_patterns.py b/tests/test_server_patterns.py index e2161a8..3f5826a 100644 --- a/tests/test_server_patterns.py +++ b/tests/test_server_patterns.py @@ -349,7 +349,7 @@ def test_pattern1_embedded_first_requires_close(cleanup_test_dirs): print("\n7. Dual access now available...") rows = _http_query(server, db_name, "SELECT count(*) AS count FROM Person") assert rows[0]["count"] == 3 - print(f" 💡 Embedded access: db.query() works") + print(" 💡 Embedded access: db.query() works") print(f" 💡 HTTP access: http://localhost:{server.get_http_port()} works") print(" 💡 Note: Server-managed databases are closed by server.stop()") diff --git a/tests/test_typed_rows.py b/tests/test_typed_rows.py index 6565511..1c4545c 100644 --- a/tests/test_typed_rows.py +++ b/tests/test_typed_rows.py @@ -163,7 +163,6 @@ def _in_memory(rows): """A result set over rows built in Java, for values SQL cannot produce.""" internal = jpype.JClass("com.arcadedb.query.sql.executor.InternalResultSet")() result_internal = jpype.JClass("com.arcadedb.query.sql.executor.ResultInternal") - hash_map = jpype.JClass("java.util.LinkedHashMap") for row in rows: java_row = result_internal() for key, value in row.items(): diff --git a/tests/test_vector.py b/tests/test_vector.py index 944b670..6f49f27 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -1079,14 +1079,6 @@ def test_lsm_cosine_distance_orthogonal_vectors(self, test_db): ), f"Orthogonal distance should be ~1.0, got {distance}" def test_lsm_euclidean_distance(self, test_db): - """Test Euclidean distance metric (JVector implementation).""" - try: - import numpy as np - - use_numpy = True - except ImportError: - use_numpy = False - test_db.command("sql", "CREATE VERTEX TYPE VectorTestEuclidean") test_db.command("sql", "CREATE PROPERTY VectorTestEuclidean.name STRING") test_db.command( diff --git a/tests/test_vector_params_verification.py b/tests/test_vector_params_verification.py index 6669493..3b7d092 100644 --- a/tests/test_vector_params_verification.py +++ b/tests/test_vector_params_verification.py @@ -265,7 +265,7 @@ def test_jvm_heap_check(self): total_memory = runtime.totalMemory() free_memory = runtime.freeMemory() - print(f"\n=== JVM Memory Stats ===") + print("\n=== JVM Memory Stats ===") print(f"Max Memory: {max_memory / (1024**3):.2f} GB ({max_memory} bytes)") print(f"Total Memory: {total_memory / (1024**2):.2f} MB") print(f"Free Memory: {free_memory / (1024**2):.2f} MB") diff --git a/tests/test_vector_sql.py b/tests/test_vector_sql.py index c752cdb..f324eac 100644 --- a/tests/test_vector_sql.py +++ b/tests/test_vector_sql.py @@ -382,7 +382,7 @@ def test_create_index_with_quantization_int8_sql(self, test_db): assert len(neighbors) == 1 vertex = neighbors[0] - vec_data = arcadedb.to_python_array(vertex.get("vec")) + assert vertex is not None # The returned vector's values are not checked: the content assert # below is disabled, so this test asserts only that the INT8 index # answers with one neighbour. @@ -801,7 +801,6 @@ def test_vector_neighbors(self, test_db): "sql", "SELECT name FROM schema:indexes WHERE typeName = 'Item'" ).to_list() ] - index_name = indexes[0] query_vec = [0.9, 0.1] @@ -867,7 +866,7 @@ def test_vector_delete_and_search_others_sql(self, test_db): ).to_list() ] # Filter for our index if multiple exist - index_name = next(idx for idx in indexes if "DocSql" in idx) + assert any("DocSql" in idx for idx in indexes), indexes # Generate 100 random vectors num_vectors = 100 diff --git a/tests/test_wheel_size.py b/tests/test_wheel_size.py index 4f891fb..8fe2102 100644 --- a/tests/test_wheel_size.py +++ b/tests/test_wheel_size.py @@ -10,8 +10,6 @@ import zipfile from pathlib import Path -import pytest - SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "verify_wheel_size.py" diff --git a/tests/test_wheel_tags.py b/tests/test_wheel_tags.py index b739b63..321a387 100644 --- a/tests/test_wheel_tags.py +++ b/tests/test_wheel_tags.py @@ -45,7 +45,15 @@ def _build(tmp_path: Path, *plat_args: str) -> Path: shutil.copy(PYTHON_DIR / "README.md", project) dist = tmp_path / "dist" subprocess.run( # nosec B603 - fixed argv (sys.executable setup.py bdist_wheel), no shell - [sys.executable, "setup.py", "-q", "bdist_wheel", "--dist-dir", str(dist), *plat_args], + [ + sys.executable, + "setup.py", + "-q", + "bdist_wheel", + "--dist-dir", + str(dist), + *plat_args, + ], cwd=project, check=True, capture_output=True, @@ -76,7 +84,9 @@ def test_the_build_without_a_platform_name_keeps_the_hosts(tmp_path): if not (PYTHON_DIR / "setup.py").exists(): return wheel = _build(tmp_path) - assert "-py3-none-" in wheel.name and not wheel.name.endswith("-any.whl"), wheel.name + assert "-py3-none-" in wheel.name and not wheel.name.endswith( + "-any.whl" + ), wheel.name def test_the_verifier_refuses_the_old_per_python_tag_and_the_any_tag(tmp_path):