diff --git a/.github/workflows/cross_build.yml b/.github/workflows/cross_build.yml index c2103a1e..5976d741 100644 --- a/.github/workflows/cross_build.yml +++ b/.github/workflows/cross_build.yml @@ -1,57 +1,57 @@ -name: cross-build +# name: cross-build -on: - push: - branches: - - improve-and-fix-workflow -# pull_request: -# types: [closed] -# branches: -# - main +# on: +# push: +# branches: +# - improve-and-fix-workflow +# # pull_request: +# # types: [closed] +# # branches: +# # - main -jobs: - wait-for-other-workflow: - name: Wait for Other Workflow - runs-on: ubuntu-latest - steps: - - name: Wait for Other Workflow to Complete - run: "echo 'Waiting for other workflow to complete...'" +# jobs: +# wait-for-other-workflow: +# name: Wait for Other Workflow +# runs-on: ubuntu-latest +# steps: +# - name: Wait for Other Workflow to Complete +# run: "echo 'Waiting for other workflow to complete...'" - build: - if: github.event.pull_request.merged == true - name: Build C release for ${{ matrix.os.name }} - strategy: - matrix: - os: - - name: ubuntu - lib: libsurrealdb_c.so - - name: macos - lib: libsurrealdb_c.dylib - - name: windows - lib: surrealdb_c.dll - runs-on: ${{ format('{0}-latest', matrix.os.name) }} - steps: - - name: Checkout Surreal C lib - uses: actions/checkout@v3 - with: - repository: surrealdb/surrealdb.c +# build: +# if: github.event.pull_request.merged == true +# name: Build C release for ${{ matrix.os.name }} +# strategy: +# matrix: +# os: +# - name: ubuntu +# lib: libsurrealdb_c.so +# - name: macos +# lib: libsurrealdb_c.dylib +# - name: windows +# lib: surrealdb_c.dll +# runs-on: ${{ format('{0}-latest', matrix.os.name) }} +# steps: +# - name: Checkout Surreal C lib +# uses: actions/checkout@v3 +# with: +# repository: surrealdb/surrealdb.c - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true +# - name: Set up Rust +# uses: actions-rs/toolchain@v1 +# with: +# toolchain: stable +# override: true - - name: Build Rust library - run: cargo build --release +# - name: Build Rust library +# run: cargo build --release - - name: Archive library - run: | - mkdir -p dist/${{ matrix.os.name }} - cp target/release/${{ matrix.os.lib }} dist/${{ matrix.os.name }}/${{ matrix.os.lib }} +# - name: Archive library +# run: | +# mkdir -p dist/${{ matrix.os.name }} +# cp target/release/${{ matrix.os.lib }} dist/${{ matrix.os.name }}/${{ matrix.os.lib }} - - name: Upload build artifacts - uses: actions/upload-artifact@v3 - with: - name: ${{ matrix.os.name }}-build - path: dist/${{ matrix.os.name }} +# - name: Upload build artifacts +# uses: actions/upload-artifact@v3 +# with: +# name: ${{ matrix.os.name }}-build +# path: dist/${{ matrix.os.name }} diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index e9adc971..2c1e4edc 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] - surrealdb-version: ["v2.0.0", "v2.1.1", "v2.1.2"] # Issue with v2.1.0 + surrealdb-version: ["v2.1.1", "v2.1.2"] # Issue with v2.1.0 name: Run unit test SurrealDB ${{ matrix.surrealdb-version }} - Python Version ${{ matrix.python-version }} steps: - name: Checkout repository @@ -33,7 +33,7 @@ jobs: - name: Run unit tests env: SURREALDB_URL: "http://localhost:8000" - run: python -m unittest discover -s tests + run: export PYTHONPATH="./src" && python -m unittest discover -s tests diff --git a/Makefile b/Makefile deleted file mode 100644 index ddaa66fc..00000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -.PHONY: test test_versions - -test: - python -m unittest discover -s tests - -test_versions: - python tests/scripts/runner.py diff --git a/README.md b/README.md index 184070da..de989708 100644 --- a/README.md +++ b/README.md @@ -46,228 +46,89 @@ View the SDK documentation [here](https://surrealdb.com/docs/integration/librari pip install surrealdb ``` -## Basic Usage -> All examples assume SurrealDB is [installed](https://surrealdb.com/install) and running on port 8000. -### Initialization -To start using the database, create an instance of SurrealDB, connect to your SurrealDB server, and specify the -namespace and database you wish to work with. Always remember to close the db connection after use. -```python -from surrealdb import SurrealDB - -# Connect to the database -db = SurrealDB(url="ws://localhost:8000") -db.connect() -db.use("namespace", "database_name") -# Your DML code... -db.close() -``` - -### Using context manager -The library supports Python’s context manager to manage connections automatically. -This ensures that connections are properly closed when the block of code finishes executing. -```python -from surrealdb import SurrealDB - -with SurrealDB(url="ws://localhost:8000") as db: - db.use("namespace", "database_name") - # Your DML code... -``` - -### Using Async -The AsyncSurrealDB supports asynchronous operations while retaining compatibility with synchronous workflows, -ensuring flexibility for any range of use cases. The APIs do not differ -```python -from surrealdb import AsyncSurrealDB - -async with AsyncSurrealDB(url="ws://localhost:8000") as db: - await db.use("namespace", "database_name") - # Your DML code... -``` -Without context manager: -```python -from surrealdb import AsyncSurrealDB - -# Connect to the database -db = AsyncSurrealDB(url="ws://localhost:8000") -await db.connect() -await db.use("namespace", "database_name") -# Your DML code... -await db.close() -``` - -### Example Usage -```python -from surrealdb import SurrealDB, GeometryPoint, Table +# Quick start -with SurrealDB(url="ws://localhost:8000") as db: - db.use("test_ns", "test_db") - auth_token = db.sign_in(username="root", password="root") +In this short guide, you will learn how to install, import, and initialize the SDK, as well as perform the basic data manipulation queries. +This guide uses the `Surreal` class, but this example would also work with `AsyncSurreal` class, with the addition of `await` in front of the class methods. - # Check token validity. This is not necessary if you called `sign_in` before. This authenticates the - # `db` instance too if `sign_in` was not previously called - db.authenticate(auth_token) - # Create an entry - person = db.create(Table("persons"), { - "Name": "John", - "Surname": "Doe", - "Location": GeometryPoint(-0.11, 22.00), - }) +## Install - print("Created person with a map:", person) - - # Get entry by Record ID - person = db.select(person.get("id")) - print("Selected a person by record id: ", person) - - # Or retrieve the entire table - persons = db.select(Table("persons")) - print("Selected all in persons table: ", persons) - - # Delete an entry by ID - _ = db.delete(persons[0].get("id")) - - # Delete all entries - _ = db.delete(Table("persons")) - - # Confirm empty table - persons = db.select(Table("persons")) - print("No Selected person: ", persons) - - # And later, we can invalidate the token - db.invalidate(auth_token) -``` - -## Connection Engines -There are 3 available connection engines you can use to connect to SurrealDb backend. It can be via Websocket, HTTP or -through embedded database connections. The connection types are simply determined by the url scheme provided in -the connection url. - -### Via Websocket -Websocket url can be `ws` or `wss` for secure connection. For example `ws://localhost:8000` and `wss://localhost:8000`. -All functionalities are available via websockets. - -### Via HTTP -HTTP url can be `http` or `https` for secure connection. For example `http://localhost:8000` and `https://localhost:8000`. -There are some functions that are not available on RPC when using HTTP but are on Websocket. This includes all -live query/notification methods. - - -### Using SurrealKV and Memory -SurrealKV and In-Memory also do not support live notifications at this time. This would be updated in a later -release. - -For Surreal KV -```python -from surrealdb import SurrealDB - -db = SurrealDB("surrealkv://path/to/dbfile.kv") -``` -For Memory -```python -from surrealdb import SurrealDB - -db = SurrealDB("mem://") -db = SurrealDB("memory://") +```sh +pip install surrealdb ``` -## Additional Examples -### Insert and Retrieve Data -```python -from surrealdb import SurrealDB - -db = SurrealDB("ws://localhost:8000") -db.connect() -db.use("example_ns", "example_db") -db.sign_in("root", "root") - -# Insert a record -new_user = {"name": "Alice", "age": 30} -inserted_record = db.insert("users", new_user) -print(f"Inserted Record: {inserted_record}") - -# Retrieve the record -retrieved_users = db.select("users") -print(f"Retrieved Users: {retrieved_users}") +## Learn the basics -db.close() -``` - -### Perform a Custom Query ```python -from surrealdb import AsyncSurrealDB -import asyncio - - -async def main(): - async with AsyncSurrealDB(url="ws://localhost:8000") as db: - await db.sign_in("root", "root") - await db.use("test", "test") - - query = "SELECT * FROM users WHERE age > $min_age;" - variables = {"min_age": 25} - - results = await db.query(query, variables) - print(f"Query Results: {results}") - -asyncio.run(main()) -``` -### Manage Authentication -```python -from surrealdb import SurrealDB - -with SurrealDB(url="ws://localhost:8000") as db: - # Sign up a new user - token = db.sign_up(username="new_user", password="secure_password") - print(f"New User Token: {token}") +# Import the Surreal class +from surrealdb import Surreal + +# Using a context manger to automatically connect and disconnect +with Surreal("ws://localhost:8000/rpc") as db: + db.signin({"username": 'root', "password": 'root'}) + db.use("namepace_test", "database_test") + + # Create a record in the person table + db.create( + "person", + { + "user": "me", + "password": "safe", + "marketing": True, + "tags": ["python", "documentation"], + }, + ) + + # Read all the records in the table + print(db.select("person")) + + # Update all records in the table + print(db.update("person", { + "user":"you", + "password":"very_safe", + "marketing": False, + "tags": ["Awesome"] + })) + + # Delete all records in the table + print(db.delete("person")) + + # You can also use the query method + # doing all of the above and more in SurrealQl - # Sign in as an existing user - token = db.sign_in(username="existing_user", password="secure_password") - print(f"Signed In Token: {token}") + # In SurrealQL you can do a direct insert + # and the table will be created if it doesn't exist - # Authenticate using a token - db.authenticate(token=token) - print("Authentication successful!") -``` - -### Live Query Notifications -```python -import asyncio -from surrealdb.surrealdb import SurrealDB - -db = SurrealDB("ws://localhost:8000") -db.connect() -db.use("example_ns", "example_db") - -# Start a live query -live_id = db.live("users") - -# Process live notifications -notifications = db.live_notifications(live_id) - -async def handle_notifications(): - while True: - notification = await notifications.get() - print(f"Live Notification: {notification}") - -# Run the notification handler -asyncio.run(handle_notifications()) + # Create + db.query(""" + insert into person { + user: 'me', + password: 'very_safe', + tags: ['python', 'documentation'] + }; + """) + + # Read + print(db.query("select * from person")) + + # Update + print(db.query(""" + update person content { + user: 'you', + password: 'more_safe', + tags: ['awesome'] + }; + """)) + + # Delete + print(db.query("delete person")) ``` -### Upserting Records -```python -from surrealdb.surrealdb import SurrealDB - -with SurrealDB("ws://localhost:8000") as db: - db.sign_in("root", "root") - db.use("example_ns", "example_db") - - upsert_data = { "name": "Charlie", "age": 35} - result = db.upsert("users", upsert_data) - print(f"Upsert Result: {result}") -``` +## Next steps +Now that you have learned the basics of the SurrealDB SDK for Python, you can learn more about the SDK and its methods [in the methods section](https://surrealdb.com/docs/sdk/python/methods) and [data types section](https://surrealdb.com/docs/sdk/python/data-types). ## Contributing Contributions to this library are welcome! If you encounter issues, have feature requests, or want to make improvements, feel free to open issues or submit pull requests. diff --git a/docker-compose.yml b/docker-compose.yml index 0b478b99..726e7367 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,28 +6,10 @@ services: environment: - SURREAL_USER=root - SURREAL_PASS=root - - SURREAL_LOG=trace - ports: - - 8300:8000 - - surrealdb_big_data: - image: surrealdb-big-data -# environment: -# - SURREAL_USER=root -# - SURREAL_PASS=root -# - SURREAL_LOG=trace - ports: - - 9000:8000 - - surrealdb_200: - image: surrealdb/surrealdb:v2.0.0 - command: "start" - environment: - - SURREAL_USER=root - - SURREAL_PASS=root - - SURREAL_LOG=trace + - SURREAL_INSECURE_FORWARD_ACCESS_ERRORS=true + - SURREAL_LOG=debug ports: - - 8200:8000 + - 8000:8000 surrealdb_121: image: surrealdb/surrealdb:v1.2.1 diff --git a/examples/async/basic_auth_example.py b/examples/async/basic_auth_example.py deleted file mode 100644 index fb89365f..00000000 --- a/examples/async/basic_auth_example.py +++ /dev/null @@ -1,22 +0,0 @@ -import asyncio - -from surrealdb import AsyncSurrealDB - - -async def main(): - async with AsyncSurrealDB(url="ws://localhost:8000") as db: - # Sign up a new user - token = await db.sign_up(username="new_user", password="secure_password") - print(f"New User Token: {token}") - - # Sign in as an existing user - token = await db.sign_in(username="existing_user", password="secure_password") - print(f"Signed In Token: {token}") - - # Authenticate using a token - await db.authenticate(token=token) - print("Authentication successful!") - - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/examples/async/basic_example.py b/examples/async/basic_example.py deleted file mode 100644 index d62b4603..00000000 --- a/examples/async/basic_example.py +++ /dev/null @@ -1,78 +0,0 @@ -from surrealdb import AsyncSurrealDB - - -async def main(): - """Example of how to use the SurrealDB client.""" - async with AsyncSurrealDB("ws://localhost:8000") as db: - await db.use("test", "test") - await db.sign_in("root", "root") - - print("Using methods") - print( - "create: ", - await db.create( - "person", - { - "user": "me", - "pass": "safe", - "marketing": True, - "tags": ["python", "documentation"], - }, - ), - ) - print("read: ", await db.select("person")) - print( - "update: ", - await db.update( - "person", - { - "user": "you", - "pass": "very_safe", - "marketing": False, - "tags": ["Awesome"], - }, - ), - ) - print("delete: ", await db.delete("person")) - - # You can also use the query method - # doing all of the above and more in SurrealQl - - # In SurrealQL you can do a direct insert - # and the table will be created if it doesn't exist - print("Using justawait db.query") - print( - "create: ", - await db.query( - """ - insert into person { - user: 'me', - pass: 'very_safe', - tags: ['python', 'documentation'] - }; - - """ - ), - ) - print("read: ", await db.query("select * from person")) - - print( - "update: ", - await db.query( - """ - update person content { - user: 'you', - pass: 'more_safe', - tags: ['awesome'] - }; - - """ - ), - ) - print("delete: ", await db.query("delete person")) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(main()) diff --git a/examples/async/basic_insert_and_retrieve.py b/examples/async/basic_insert_and_retrieve.py deleted file mode 100644 index d5710147..00000000 --- a/examples/async/basic_insert_and_retrieve.py +++ /dev/null @@ -1,25 +0,0 @@ -import asyncio - -from surrealdb import AsyncSurrealDB - - -async def main(): - db = AsyncSurrealDB("ws://localhost:8000") - await db.connect() - await db.use("example_ns", "example_db") - await db.sign_in("root", "root") - - # Insert a record - new_user = {"name": "Alice", "age": 30} - inserted_record = await db.insert("users", new_user) - print(f"Inserted Record: {inserted_record}") - - # Retrieve the record - retrieved_users = await db.select("users") - print(f"Retrieved Users: {retrieved_users}") - - await db.close() - - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/examples/async/basic_query_example.py b/examples/async/basic_query_example.py deleted file mode 100644 index 384cabba..00000000 --- a/examples/async/basic_query_example.py +++ /dev/null @@ -1,17 +0,0 @@ -import asyncio -from surrealdb import AsyncSurrealDB - - -async def main(): - async with AsyncSurrealDB(url="ws://localhost:8000") as db: - await db.sign_in("root", "root") - await db.use("test", "test") - - query = "SELECT * FROM users WHERE age > min_age;" - variables = {"min_age": 25} - - results = await db.query(query, variables) - print(f"Query Results: {results}") - - -asyncio.run(main()) diff --git a/examples/async/basic_upsert_example.py b/examples/async/basic_upsert_example.py deleted file mode 100644 index 56df0156..00000000 --- a/examples/async/basic_upsert_example.py +++ /dev/null @@ -1,15 +0,0 @@ -import asyncio - -from surrealdb import AsyncSurrealDB - -async def main(): - async with AsyncSurrealDB("ws://localhost:8000") as db: - db.sign_in("root", "root") - db.use("example_ns", "example_db") - - upsert_data = {"name": "Charlie", "age": 35} - result = db.upsert("users", upsert_data) - print(f"Upsert Result: {result}") - -if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file diff --git a/examples/notebook_example.ipynb b/examples/notebook_example.ipynb deleted file mode 100644 index 4b980785..00000000 --- a/examples/notebook_example.ipynb +++ /dev/null @@ -1,98 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Example of how to use the SurrealDB client in a notebook" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from surrealdb import AsyncSurrealDB\n", - "\n", - "db = AsyncSurrealDB(\"ws://localhost:8000\")\n", - "\n", - "await db.connect()\n", - "\n", - "await db.use(\"test\", \"test\")\n", - "\n", - "await db.sign_in(\"root\", \"root\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await db.create(\n", - " \"person\",\n", - " {\n", - " \"user\": \"me\",\n", - " \"pass\": \"safe\",\n", - " \"marketing\": True,\n", - " \"tags\": [\"python\", \"documentation\"],\n", - " },\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await db.select(\"person\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await db.update(\n", - " \"person\",\n", - " {\"user\": \"you\", \"pass\": \"very_safe\", \"marketing\": False, \"tags\": [\"Awesome\"]},\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "await db.delete(\"person\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/sync/basic_auth_example.py b/examples/sync/basic_auth_example.py deleted file mode 100644 index fc5e23e2..00000000 --- a/examples/sync/basic_auth_example.py +++ /dev/null @@ -1,20 +0,0 @@ -from surrealdb import SurrealDB - - -def main(): - with SurrealDB(url="ws://localhost:8000") as db: - # Sign up a new user - token = db.sign_up(username="new_user", password="secure_password") - print(f"New User Token: {token}") - - # Sign in as an existing user - token = db.sign_in(username="existing_user", password="secure_password") - print(f"Signed In Token: {token}") - - # Authenticate using a token - db.authenticate(token=token) - print("Authentication successful!") - - -if __name__ == '__main__': - main() diff --git a/examples/sync/basic_example.py b/examples/sync/basic_example.py deleted file mode 100644 index a79bde38..00000000 --- a/examples/sync/basic_example.py +++ /dev/null @@ -1,75 +0,0 @@ -from surrealdb import SurrealDB - - -def main(): - with SurrealDB("ws://localhost:8000") as db: - db.use("test", "test") - token = db.sign_in("root", "root") - - print("Using methods") - print( - "create: ", - db.create( - "person", - { - "user": "me", - "pass": "safe", - "marketing": True, - "tags": ["python", "documentation"], - }, - ), - ) - print("read: ", db.select("person")) - print( - "update: ", - db.update( - "person", - { - "user": "you", - "pass": "very_safe", - "marketing": False, - "tags": ["Awesome"], - }, - ), - ) - print("delete: ", db.delete("person")) - - # You can also use the query method - # doing all of the above and more in SurrealQl - - # In SurrealQL you can do a direct insert - # and the table will be created if it doesn't exist - print("Using just db.query") - print( - "create: ", - db.query( - """ - insert into person { - user: 'me', - pass: 'very_safe', - tags: ['python', 'documentation'] - }; - - """ - ), - ) - print("read: ", db.query("select * from person")) - - print( - "update: ", - db.query( - """ - update person content { - user: 'you', - pass: 'more_safe', - tags: ['awesome'] - }; - - """ - ), - ) - print("delete: ", db.query("delete person")) - - -if __name__ == '__main__': - main() diff --git a/examples/sync/basic_insert_and_retrieve.py b/examples/sync/basic_insert_and_retrieve.py deleted file mode 100644 index 9f69b471..00000000 --- a/examples/sync/basic_insert_and_retrieve.py +++ /dev/null @@ -1,23 +0,0 @@ -from surrealdb import SurrealDB - - -def main(): - db = SurrealDB("ws://localhost:8000") - db.connect() - db.use("example_ns", "example_db") - db.sign_in("root", "root") - - # Insert a record - new_user = {"name": "Alice", "age": 30} - inserted_record = db.insert("users", new_user) - print(f"Inserted Record: {inserted_record}") - - # Retrieve the record - retrieved_users = db.select("users") - print(f"Retrieved Users: {retrieved_users}") - - db.close() - - -if __name__ == '__main__': - main() diff --git a/examples/sync/basic_query_example.py b/examples/sync/basic_query_example.py deleted file mode 100644 index f3f502fa..00000000 --- a/examples/sync/basic_query_example.py +++ /dev/null @@ -1,17 +0,0 @@ -from surrealdb import SurrealDB - - -def main(): - with SurrealDB(url="ws://localhost:8000") as db: - db.sign_in("root", "root") - db.use("test", "test") - - query = "SELECT * FROM users WHERE age > min_age;" - variables = {"min_age": 25} - - results = db.query(query, variables) - print(f"Query Results: {results}") - - -if __name__ == '__main__': - main() diff --git a/examples/sync/basic_upsert_example.py b/examples/sync/basic_upsert_example.py deleted file mode 100644 index 58658aaf..00000000 --- a/examples/sync/basic_upsert_example.py +++ /dev/null @@ -1,9 +0,0 @@ -from surrealdb.surrealdb import SurrealDB - -with SurrealDB("ws://localhost:8000") as db: - db.sign_in("root", "root") - db.use("example_ns", "example_db") - - upsert_data = {"name": "Charlie", "age": 35} - result = db.upsert("users", upsert_data) - print(f"Upsert Result: {result}") diff --git a/get_latest_version.py b/get_latest_version.py deleted file mode 100644 index 3f18b4d9..00000000 --- a/get_latest_version.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import pathlib -from typing import Tuple, List, Union - -import requests - - -def get_latest_version_number() -> str: - """ - Gets the latest pip version number from the pypi server. - Returns: (str) the version of the latest pip module - """ - req = requests.get("https://pypi.org/pypi/surrealdb-beta/json") - return req.json()["info"]["version"] - - -def write_version_to_file(version_number: str) -> None: - """ - Writes the version to the VERSION.txt file. - Args: - version_number: (str) the version to be written to the file - Returns: None - """ - version_file_path: str = str(pathlib.Path(__file__).parent.absolute()) + "/surrealdb/VERSION.txt" - - if os.path.exists(version_file_path): - os.remove(version_file_path) - - with open(version_file_path, "w") as f: - f.write(f"VERSION='{version_number}'") - - -def unpack_version_number(version_string: str) -> Tuple[int, int, int]: - """ - Unpacks the version number converting it into a Tuple of integers. - Args: - version_string: (str) the version to be unpacked - Returns: (Tuple[int, int, int]) the version number - """ - version_buffer: List[str] = version_string.split(".") - return int(version_buffer[0]), int(version_buffer[1]), int(version_buffer[2]) - - -def pack_version_number(version_buffer: Union[Tuple[int, int, int], List[int]]) -> str: - """ - Packs the version number into a string. - Args: - version_buffer: (Union[Tuple[int, int, int], List[int]]) the version to be packed - Returns: (str) the packed version number - """ - return f"{version_buffer[0]}.{version_buffer[1]}.{version_buffer[2]}" - - -def increase_version_number(version_buffer: Union[Tuple[int, int, int], List[int]]) -> List[int]: - """ - Increases the number of the version with an increment of 0.0.1. - Args: - version_buffer: (Union[Tuple[int, int, int], List[int]]) the verion to be increased - Returns: (List[int]) the updated version - """ - first: int = version_buffer[0] - second: int = version_buffer[1] - third: int = version_buffer[2] - - third += 1 - if third >= 10: - third = 0 - second += 1 - if second >= 10: - second = 0 - first += 1 - - return [first, second, third] - - -if __name__ == "__main__": - write_version_to_file( - version_number=pack_version_number( - version_buffer=increase_version_number( - version_buffer=unpack_version_number( - version_string=get_latest_version_number() - ) - ) - ) - ) diff --git a/libsrc/libsurrealdb_c.dylib b/libsrc/libsurrealdb_c.dylib deleted file mode 100755 index 73885b12..00000000 Binary files a/libsrc/libsurrealdb_c.dylib and /dev/null differ diff --git a/libsrc/surrealdb.h b/libsrc/surrealdb.h deleted file mode 100644 index f27490e2..00000000 --- a/libsrc/surrealdb.h +++ /dev/null @@ -1,440 +0,0 @@ -#include -#include -#include -#include - -#define sr_SR_NONE 0 - -#define sr_SR_CLOSED -1 - -#define sr_SR_ERROR -2 - -#define sr_SR_FATAL -3 - -typedef enum sr_action { - SR_ACTION_CREATE, - SR_ACTION_UPDATE, - SR_ACTION_DELETE, -} sr_action; - -typedef struct sr_opaque_object_internal_t sr_opaque_object_internal_t; - -typedef struct sr_RpcStream sr_RpcStream; - -/** - * may be sent across threads, but must not be aliased - */ -typedef struct sr_stream_t sr_stream_t; - -/** - * The object representing a Surreal connection - * - * It is safe to be referenced from multiple threads - * If any operation, on any thread returns SR_FATAL then the connection is poisoned and must not be used again. - * (use will cause the program to abort) - * - * should be freed with sr_surreal_disconnect - */ -typedef struct sr_surreal_t sr_surreal_t; - -/** - * The object representing a Surreal connection - * - * It is safe to be referenced from multiple threads - * If any operation, on any thread returns SR_FATAL then the connection is poisoned and must not be used again. - * (use will cause the program to abort) - * - * should be freed with sr_surreal_disconnect - */ -typedef struct sr_surreal_rpc_t sr_surreal_rpc_t; - -typedef char *sr_string_t; - -typedef struct sr_object_t { - struct sr_opaque_object_internal_t *_0; -} sr_object_t; - -typedef enum sr_number_t_Tag { - SR_NUMBER_INT, - SR_NUMBER_FLOAT, -} sr_number_t_Tag; - -typedef struct sr_number_t { - sr_number_t_Tag tag; - union { - struct { - int64_t sr_number_int; - }; - struct { - double sr_number_float; - }; - }; -} sr_number_t; - -typedef struct sr_duration_t { - uint64_t secs; - uint32_t nanos; -} sr_duration_t; - -typedef struct sr_uuid_t { - uint8_t _0[16]; -} sr_uuid_t; - -typedef struct sr_bytes_t { - uint8_t *arr; - int len; -} sr_bytes_t; - -typedef enum sr_id_t_Tag { - SR_ID_NUMBER, - SR_ID_STRING, - SR_ID_ARRAY, - SR_ID_OBJECT, -} sr_id_t_Tag; - -typedef struct sr_id_t { - sr_id_t_Tag tag; - union { - struct { - int64_t sr_id_number; - }; - struct { - sr_string_t sr_id_string; - }; - struct { - struct sr_array_t *sr_id_array; - }; - struct { - struct sr_object_t sr_id_object; - }; - }; -} sr_id_t; - -typedef struct sr_thing_t { - sr_string_t table; - struct sr_id_t id; -} sr_thing_t; - -typedef enum sr_value_t_Tag { - SR_VALUE_NONE, - SR_VALUE_NULL, - SR_VALUE_BOOL, - SR_VALUE_NUMBER, - SR_VALUE_STRAND, - SR_VALUE_DURATION, - SR_VALUE_DATETIME, - SR_VALUE_UUID, - SR_VALUE_ARRAY, - SR_VALUE_OBJECT, - SR_VALUE_BYTES, - SR_VALUE_THING, -} sr_value_t_Tag; - -typedef struct sr_value_t { - sr_value_t_Tag tag; - union { - struct { - bool sr_value_bool; - }; - struct { - struct sr_number_t sr_value_number; - }; - struct { - sr_string_t sr_value_strand; - }; - struct { - struct sr_duration_t sr_value_duration; - }; - struct { - sr_string_t sr_value_datetime; - }; - struct { - struct sr_uuid_t sr_value_uuid; - }; - struct { - struct sr_array_t *sr_value_array; - }; - struct { - struct sr_object_t sr_value_object; - }; - struct { - struct sr_bytes_t sr_value_bytes; - }; - struct { - struct sr_thing_t sr_value_thing; - }; - }; -} sr_value_t; - -typedef struct sr_array_t { - struct sr_value_t *arr; - int len; -} sr_array_t; - -/** - * when code = 0 there is no error - */ -typedef struct sr_SurrealError { - int code; - sr_string_t msg; -} sr_SurrealError; - -typedef struct sr_arr_res_t { - struct sr_array_t ok; - struct sr_SurrealError err; -} sr_arr_res_t; - -typedef struct sr_option_t { - bool strict; - uint8_t query_timeout; - uint8_t transaction_timeout; -} sr_option_t; - -typedef struct sr_notification_t { - struct sr_uuid_t query_id; - enum sr_action action; - struct sr_value_t data; -} sr_notification_t; - -/** - * connects to a local, remote, or embedded database - * - * if any function returns SR_FATAL, this must not be used (except to drop) (TODO: check this is safe) doing so will cause the program to abort - * - * # Examples - * - * ```c - * sr_string_t err; - * sr_surreal_t *db; - * - * // connect to in-memory instance - * if (sr_connect(&err, &db, "mem://") < 0) { - * printf("error connecting to db: %s\n", err); - * return 1; - * } - * - * // connect to surrealkv file - * if (sr_connect(&err, &db, "surrealkv://test.skv") < 0) { - * printf("error connecting to db: %s\n", err); - * return 1; - * } - * - * // connect to surrealdb server - * if (sr_connect(&err, &db, "wss://localhost:8000") < 0) { - * printf("error connecting to db: %s\n", err); - * return 1; - * } - * - * sr_surreal_disconnect(db); - * ``` - */ -int sr_connect(sr_string_t *err_ptr, - struct sr_surreal_t **surreal_ptr, - const char *endpoint); - -/** - * disconnect a database connection - * note: the Surreal object must not be used after this function has been called - * any object allocations will still be valid, and should be freed, using the appropriate function - * TODO: check if Stream can be freed after disconnection because of rt - * - * # Examples - * - * ```c - * sr_surreal_t *db; - * // connect - * disconnect(db); - * ``` - */ -void sr_surreal_disconnect(struct sr_surreal_t *db); - -/** - * create a record - * - */ -int sr_create(const struct sr_surreal_t *db, - sr_string_t *err_ptr, - struct sr_object_t **res_ptr, - const char *resource, - const struct sr_object_t *content); - -/** - * make a live selection - * if successful sets *stream_ptr to be an exclusive reference to an opaque Stream object - * which can be moved accross threads but not aliased - * - * # Examples - * - * sr_stream_t *stream; - * if (sr_select_live(db, &err, &stream, "foo") < 0) - * { - * printf("%s", err); - * return 1; - * } - * - * sr_notification_t not ; - * if (sr_stream_next(stream, ¬ ) > 0) - * { - * sr_print_notification(¬ ); - * } - * sr_stream_kill(stream); - */ -int sr_select_live(const struct sr_surreal_t *db, - sr_string_t *err_ptr, - struct sr_stream_t **stream_ptr, - const char *resource); - -int sr_query(const struct sr_surreal_t *db, - sr_string_t *err_ptr, - struct sr_arr_res_t **res_ptr, - const char *query, - const struct sr_object_t *vars); - -/** - * select a resource - * - * can be used to select everything from a table or a single record - * writes values to *res_ptr, and returns number of values - * result values are allocated by Surreal and must be freed with sr_free_arr - * - * # Examples - * - * ```c - * sr_surreal_t *db; - * sr_string_t err; - * sr_value_t *foos; - * int len = sr_select(db, &err, &foos, "foo"); - * if (len < 0) { - * printf("%s", err); - * return 1; - * } - * ``` - * for (int i = 0; i < len; i++) - * { - * sr_value_print(&foos[i]); - * } - * sr_free_arr(foos, len); - */ -int sr_select(const struct sr_surreal_t *db, - sr_string_t *err_ptr, - struct sr_value_t **res_ptr, - const char *resource); - -/** - * select database - * NOTE: namespace must be selected first with sr_use_ns - * - * # Examples - * ```c - * sr_surreal_t *db; - * sr_string_t err; - * if (sr_use_db(db, &err, "test") < 0) - * { - * printf("%s", err); - * return 1; - * } - * ``` - */ -int sr_use_db(const struct sr_surreal_t *db, sr_string_t *err_ptr, const char *query); - -/** - * select namespace - * NOTE: database must be selected before use with sr_use_db - * - * # Examples - * ```c - * sr_surreal_t *db; - * sr_string_t err; - * if (sr_use_ns(db, &err, "test") < 0) - * { - * printf("%s", err); - * return 1; - * } - * ``` - */ -int sr_use_ns(const struct sr_surreal_t *db, sr_string_t *err_ptr, const char *query); - -/** - * returns the db version - * NOTE: version is allocated in Surreal and must be freed with sr_free_string - * # Examples - * ```c - * sr_surreal_t *db; - * sr_string_t err; - * sr_string_t ver; - * - * if (sr_version(db, &err, &ver) < 0) - * { - * printf("%s", err); - * return 1; - * } - * printf("%s", ver); - * sr_free_string(ver); - * ``` - */ -int sr_version(const struct sr_surreal_t *db, sr_string_t *err_ptr, sr_string_t *res_ptr); - -int sr_surreal_rpc_new(sr_string_t *err_ptr, - struct sr_surreal_rpc_t **surreal_ptr, - const char *endpoint, - struct sr_option_t options); - -/** - * execute rpc - * - * free result with sr_free_byte_arr - */ -int sr_surreal_rpc_execute(const struct sr_surreal_rpc_t *self, - sr_string_t *err_ptr, - uint8_t **res_ptr, - const uint8_t *ptr, - int len); - -int sr_surreal_rpc_notifications(const struct sr_surreal_rpc_t *self, - sr_string_t *err_ptr, - struct sr_RpcStream **stream_ptr); - -void sr_surreal_rpc_free(struct sr_surreal_rpc_t *ctx); - -void sr_free_arr(struct sr_value_t *ptr, int len); - -void sr_free_bytes(struct sr_bytes_t bytes); - -void sr_free_byte_arr(uint8_t *ptr, int len); - -void sr_print_notification(const struct sr_notification_t *notification); - -const struct sr_value_t *sr_object_get(const struct sr_object_t *obj, const char *key); - -struct sr_object_t sr_object_new(void); - -void sr_object_insert(struct sr_object_t *obj, const char *key, const struct sr_value_t *value); - -void sr_object_insert_str(struct sr_object_t *obj, const char *key, const char *value); - -void sr_object_insert_int(struct sr_object_t *obj, const char *key, int value); - -void sr_object_insert_float(struct sr_object_t *obj, const char *key, float value); - -void sr_object_insert_double(struct sr_object_t *obj, const char *key, double value); - -void sr_free_object(struct sr_object_t obj); - -void sr_free_arr_res(struct sr_arr_res_t res); - -void sr_free_arr_res_arr(struct sr_arr_res_t *ptr, int len); - -/** - * blocks until next item is recieved on stream - * will return 1 and write notification to notification_ptr is recieved - * will return SR_NONE if the stream is closed - */ -int sr_stream_next(struct sr_stream_t *self, struct sr_notification_t *notification_ptr); - -void sr_stream_kill(struct sr_stream_t *stream); - -void sr_free_string(sr_string_t string); - -void sr_value_print(const struct sr_value_t *val); - -bool sr_value_eq(const struct sr_value_t *lhs, const struct sr_value_t *rhs); diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 4093a96c..00000000 --- a/poetry.lock +++ /dev/null @@ -1,555 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. - -[[package]] -name = "black" -version = "24.10.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "cbor2" -version = "5.6.5" -description = "CBOR (de)serializer with extensive tag support" -optional = false -python-versions = ">=3.8" -files = [ - {file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"}, - {file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"}, - {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"}, - {file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"}, - {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"}, - {file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"}, - {file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"}, - {file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"}, - {file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"}, - {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"}, - {file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"}, - {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"}, - {file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"}, - {file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"}, - {file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"}, - {file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"}, - {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"}, - {file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"}, - {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"}, - {file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"}, - {file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"}, - {file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"}, - {file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"}, - {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"}, - {file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"}, - {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"}, - {file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"}, - {file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"}, - {file = "cbor2-5.6.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff"}, - {file = "cbor2-5.6.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2"}, - {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb"}, - {file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb"}, - {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862"}, - {file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568"}, - {file = "cbor2-5.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6"}, - {file = "cbor2-5.6.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b"}, - {file = "cbor2-5.6.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c"}, - {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459"}, - {file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712"}, - {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd"}, - {file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f"}, - {file = "cbor2-5.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4"}, - {file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"}, - {file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"}, -] - -[package.extras] -benchmarks = ["pytest-benchmark (==4.0.0)"] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)", "typing-extensions"] -test = ["coverage (>=7)", "hypothesis", "pytest"] - -[[package]] -name = "certifi" -version = "2024.8.30" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "mypy" -version = "1.13.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "packaging" -version = "24.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "ruff" -version = "0.8.0" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:fcb1bf2cc6706adae9d79c8d86478677e3bbd4ced796ccad106fd4776d395fea"}, - {file = "ruff-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:295bb4c02d58ff2ef4378a1870c20af30723013f441c9d1637a008baaf928c8b"}, - {file = "ruff-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b1f1c76b47c18fa92ee78b60d2d20d7e866c55ee603e7d19c1e991fad933a9a"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb0d4f250a7711b67ad513fde67e8870109e5ce590a801c3722580fe98c33a99"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e55cce9aa93c5d0d4e3937e47b169035c7e91c8655b0974e61bb79cf398d49c"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f4cd64916d8e732ce6b87f3f5296a8942d285bbbc161acee7fe561134af64f9"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c5c1466be2a2ebdf7c5450dd5d980cc87c8ba6976fb82582fea18823da6fa362"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2dabfd05b96b7b8f2da00d53c514eea842bff83e41e1cceb08ae1966254a51df"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:facebdfe5a5af6b1588a1d26d170635ead6892d0e314477e80256ef4a8470cf3"}, - {file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"}, - {file = "ruff-0.8.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85e654f0ded7befe2d61eeaf3d3b1e4ef3894469cd664ffa85006c7720f1e4a2"}, - {file = "ruff-0.8.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:83a55679c4cb449fa527b8497cadf54f076603cc36779b2170b24f704171ce70"}, - {file = "ruff-0.8.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:812e2052121634cf13cd6fddf0c1871d0ead1aad40a1a258753c04c18bb71bbd"}, - {file = "ruff-0.8.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:780d5d8523c04202184405e60c98d7595bdb498c3c6abba3b6d4cdf2ca2af426"}, - {file = "ruff-0.8.0-py3-none-win32.whl", hash = "sha256:5fdb6efecc3eb60bba5819679466471fd7d13c53487df7248d6e27146e985468"}, - {file = "ruff-0.8.0-py3-none-win_amd64.whl", hash = "sha256:582891c57b96228d146725975fbb942e1f30a0c4ba19722e692ca3eb25cc9b4f"}, - {file = "ruff-0.8.0-py3-none-win_arm64.whl", hash = "sha256:ba93e6294e9a737cd726b74b09a6972e36bb511f9a102f1d9a7e1ce94dd206a6"}, - {file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"}, -] - -[[package]] -name = "tomli" -version = "2.1.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, -] - -[[package]] -name = "types-requests" -version = "2.32.0.20241016" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, - {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "websockets" -version = "14.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29"}, - {file = "websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179"}, - {file = "websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434"}, - {file = "websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10"}, - {file = "websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e"}, - {file = "websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512"}, - {file = "websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac"}, - {file = "websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23"}, - {file = "websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e"}, - {file = "websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09"}, - {file = "websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed"}, - {file = "websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d"}, - {file = "websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05"}, - {file = "websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0"}, - {file = "websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f"}, - {file = "websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9"}, - {file = "websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b"}, - {file = "websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a"}, - {file = "websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6"}, - {file = "websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0"}, - {file = "websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a"}, - {file = "websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6"}, - {file = "websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc"}, - {file = "websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4"}, - {file = "websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979"}, - {file = "websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8"}, - {file = "websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7"}, - {file = "websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d"}, - {file = "websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370"}, - {file = "websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1"}, - {file = "websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5"}, - {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, - {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10" -content-hash = "af463dda94c6c91cb8859c24a4a7e5ee1fbee65e44a0250609cdff241e6262d5" diff --git a/pyproject.toml b/pyproject.toml index f5223978..3c132870 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,36 +1,49 @@ -[tool.poetry] +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] name = "surrealdb" -version = "0.4.3" -description = "The official SurrealDB library for Python." +description = "SurrealDB python client" +authors = [ + { name = "Maxwell Flitton", email = "maxwellflitton@gmail.com" } +] +license = {text = "MIT License"} readme = "README.md" -authors = ["SurrealDB"] -license = "Apache-2.0" -repository = "https://github.com/surrealdb/surrealdb.py" -documentation = "https://surrealdb.com/docs/sdk/python" -keywords = ["SurrealDB", "Database"] +keywords = ["surrealdb", "SurrealDB", "surrealDB", "Surrealdb"] classifiers = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", "Operating System :: OS Independent", - "Topic :: Database", - "Topic :: Database :: Front-Ends", - "Topic :: Software Development", - "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "aiohappyeyeballs==2.4.4", + "aiohttp==3.11.11", + "aiosignal==1.3.2", + "async-timeout==5.0.1", + "attrs==25.1.0", + "cbor2==5.6.5", + "Cerberus==1.3.7", + "certifi==2024.12.14", + "charset-normalizer==3.4.1", + "frozenlist==1.5.0", + "idna==3.10", + "marshmallow==3.26.0", + "multidict==6.1.0", + "packaging==24.2", + "propcache==0.2.1", + "pytz==2024.2", + "requests==2.32.3", + "typing_extensions==4.12.2", + "urllib3==2.3.0", + "websockets==14.2", + "yarl==1.18.3" ] -[build-system] -requires = ["setuptools", "wheel"] +[project.urls] +Homepage = "https://github.com/surrealdb/surrealdb.py" -[tool.poetry.dependencies] -python = "^3.10" -requests = "^2.32.3" -cbor2 = "^5.6.5" -websockets = "^14.1" +[tool.setuptools.packages.find] +where = ["src"] -[tool.poetry.group.dev] -[tool.poetry.group.dev.dependencies] -ruff = "^0.8.0" -black = "^24.10.0" -mypy = "^1.13.0" -types-requests = "^2.32.0.20241016" +# [project.scripts] +# sdblpy = "sblpy.cli.entrypoint:main" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5ce8445a..0675b18f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,21 @@ +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiosignal==1.3.2 +async-timeout==5.0.1 +attrs==25.1.0 cbor2==5.6.5 +Cerberus==1.3.7 certifi==2024.12.14 -charset-normalizer==3.4.0 -docker==7.1.0 +charset-normalizer==3.4.1 +frozenlist==1.5.0 idna==3.10 -mypy==1.13.0 -mypy-extensions==1.0.0 +marshmallow==3.26.0 +multidict==6.1.0 +packaging==24.2 +propcache==0.2.1 pytz==2024.2 requests==2.32.3 -semantic-version==2.10.0 -setuptools>=70.0.0 -setuptools-rust==1.6.0 -tomli==2.2.1 -types-pytz==2024.2.0.20241003 -types-requests==2.32.0.20241016 typing_extensions==4.12.2 -urllib3==2.2.3 -websockets==14.1 +urllib3==2.3.0 +websockets==14.2 +yarl==1.18.3 diff --git a/scripts/download_snapshots.sh b/scripts/download_snapshots.sh deleted file mode 100644 index 21f477f8..00000000 --- a/scripts/download_snapshots.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -# navigate to directory -SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" -cd $SCRIPTPATH - -cd .. -cd tests - -if [ -d "./db_snapshots" ]; then - echo "DB snapshots are already present" - rm -rf ./db_snapshots -fi - -dockpack pull -i maxwellflitton/surrealdb-data -d ./db_snapshots diff --git a/scripts/full_test_runner.sh b/scripts/full_test_runner.sh deleted file mode 100644 index 2ac10f59..00000000 --- a/scripts/full_test_runner.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# navigate to directory -SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" -cd $SCRIPTPATH - -cd .. - -docker-compose up -d -sleep 5 - -python3 -m unittest discover -docker-compose down - -# deactivate -# rm -f testvenv diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 197b5b17..76e99975 100644 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -5,8 +5,7 @@ SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" cd $SCRIPTPATH cd .. - -#CONNECTION_PORT - -export PYTHONPATH="." -python tests/scripts/runner.py +cd src +export PYTHONPATH=$(pwd) +cd .. +python -m unittest discover diff --git a/surrealdb/data/types/__init__.py b/scripts/setup.sh similarity index 100% rename from surrealdb/data/types/__init__.py rename to scripts/setup.sh diff --git a/setup.py b/setup.py deleted file mode 100644 index d79b3c68..00000000 --- a/setup.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -import pathlib - -from setuptools import setup - - -with open("README.md", "r") as fh: - long_description = fh.read() - - -with open(str(pathlib.Path(__file__).parent.absolute()) + "/surrealdb/VERSION.txt", "r") as fh: - version = fh.read().split("=")[1].replace("'", "") - - -setup( - name="surrealdb-beta", - author="Maxwell Flitton", - author_email="maxwell@gmail.com", - description="SurrealDB python client.", - long_description=long_description, - long_description_content_type="text/markdown", - version=version, - packages=[ - "surrealdb", - "surrealdb.data", - "surrealdb.data.types" - ], - package_data={ - "surrealdb": ["binaries/*"], - }, - install_requires=[ - "cbor2==5.6.5", - "certifi==2024.12.14", - "charset-normalizer==3.4.0", - "idna==3.10", - "pytz==2024.2", - "requests==2.32.3", - "semantic-version==2.10.0", - "tomli==2.2.1", - "types-pytz==2024.2.0.20241003", - "types-requests==2.32.0.20241016", - "typing_extensions==4.12.2", - "urllib3==2.2.3", - "websockets==14.1", - ], - # rust extensions are not zip safe, just like C-extensions. - zip_safe=False, - include_package_data=True -) diff --git a/tests/integration/__init__.py b/src/__init__.py similarity index 100% rename from tests/integration/__init__.py rename to src/__init__.py diff --git a/src/surrealdb/__init__.py b/src/surrealdb/__init__.py new file mode 100644 index 00000000..2e47afcf --- /dev/null +++ b/src/surrealdb/__init__.py @@ -0,0 +1,85 @@ +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.connections.url import Url, UrlScheme +from typing import Union, Optional + +from surrealdb.data.types.table import Table +from surrealdb.data.types.constants import * +from surrealdb.data.types.datetime import DateTimeCompact +from surrealdb.data.types.duration import Duration +from surrealdb.data.types.future import Future +from surrealdb.data.types.geometry import Geometry +from surrealdb.data.types.range import Range +from surrealdb.data.types.record_id import RecordID + +class AsyncSurrealDBMeta(type): + + def __call__(cls, *args, **kwargs): + # Ensure `url` is provided as either an arg or kwarg + if len(args) > 0: + url = args[0] # Assume the first positional argument is `url` + else: + url = kwargs.get("url") + + if url is None: + raise ValueError("The 'url' parameter is required to initialise SurrealDB.") + + constructed_url = Url(url) + + + # Extract `max_size` with a default if not explicitly provided + max_size = kwargs.get("max_size", 2 ** 20) + + if constructed_url.scheme == UrlScheme.HTTP or constructed_url.scheme == UrlScheme.HTTPS: + return AsyncHttpSurrealConnection(url=url) + elif constructed_url.scheme == UrlScheme.WS or constructed_url.scheme == UrlScheme.WSS: + return AsyncWsSurrealConnection(url=url, max_size=max_size) + else: + raise ValueError(f"Unsupported protocol in URL: {url}. Use 'ws://' or 'http://'.") + + +class BlockingSurrealDBMeta(type): + + def __call__(cls, *args, **kwargs): + # Ensure `url` is provided as either an arg or kwarg + if len(args) > 0: + url = args[0] # Assume the first positional argument is `url` + else: + url = kwargs.get("url") + + if url is None: + raise ValueError("The 'url' parameter is required to initialise SurrealDB.") + + constructed_url = Url(url) + + + # Extract `max_size` with a default if not explicitly provided + max_size = kwargs.get("max_size", 2 ** 20) + + if constructed_url.scheme == UrlScheme.HTTP or constructed_url.scheme == UrlScheme.HTTPS: + return BlockingHttpSurrealConnection(url=url) + elif constructed_url.scheme == UrlScheme.WS or constructed_url.scheme == UrlScheme.WSS: + return BlockingWsSurrealConnection(url=url, max_size=max_size) + else: + raise ValueError(f"Unsupported protocol in URL: {url}. Use 'ws://' or 'http://'.") + +def Surreal(url: Optional[str] = None, max_size: int = 2 ** 20) -> Union[BlockingWsSurrealConnection, BlockingHttpSurrealConnection]: + constructed_url = Url(url) + if constructed_url.scheme == UrlScheme.HTTP or constructed_url.scheme == UrlScheme.HTTPS: + return BlockingHttpSurrealConnection(url=url) + elif constructed_url.scheme == UrlScheme.WS or constructed_url.scheme == UrlScheme.WSS: + return BlockingWsSurrealConnection(url=url, max_size=max_size) + else: + raise ValueError(f"Unsupported protocol in URL: {url}. Use 'ws://' or 'http://'.") + + +def AsyncSurreal(url: Optional[str] = None, max_size: int = 2 ** 20) -> Union[AsyncWsSurrealConnection, AsyncHttpSurrealConnection]: + constructed_url = Url(url) + if constructed_url.scheme == UrlScheme.HTTP or constructed_url.scheme == UrlScheme.HTTPS: + return AsyncHttpSurrealConnection(url=url) + elif constructed_url.scheme == UrlScheme.WS or constructed_url.scheme == UrlScheme.WSS: + return AsyncWsSurrealConnection(url=url, max_size=max_size) + else: + raise ValueError(f"Unsupported protocol in URL: {url}. Use 'ws://' or 'http://'.") diff --git a/tests/integration/async/__init__.py b/src/surrealdb/connections/__init__.py similarity index 100% rename from tests/integration/async/__init__.py rename to src/surrealdb/connections/__init__.py diff --git a/src/surrealdb/connections/async_http.py b/src/surrealdb/connections/async_http.py new file mode 100644 index 00000000..9047f9d4 --- /dev/null +++ b/src/surrealdb/connections/async_http.py @@ -0,0 +1,334 @@ +import uuid +from typing import Optional, Any, Dict, Union, List + +import aiohttp + +from surrealdb.connections.async_template import AsyncTemplate +from surrealdb.connections.url import Url +from surrealdb.connections.utils_mixin import UtilsMixin +from surrealdb.data.cbor import decode +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class AsyncHttpSurrealConnection(AsyncTemplate, UtilsMixin): + """ + A single async connection to a SurrealDB instance using HTTP. To be used once and discarded. + + # Notes + A new HTTP session is created for each query to send a request to the SurrealDB server. + + Attributes: + url: The URL of the database to process queries for. + max_size: The maximum size of the connection payload. + id: The ID of the connection. + """ + + def __init__( + self, + url: str, + ) -> None: + """ + Constructor for the AsyncHttpSurrealConnection class. + + :param url: (str) The URL of the database to process queries for. + """ + self.url: Url = Url(url) + self.raw_url: str = self.url.raw_url + self.host: str = self.url.hostname + self.port: Optional[int] = self.url.port + self.token: Optional[str] = None + self.id: str = str(uuid.uuid4()) + self.namespace: Optional[str] = None + self.database: Optional[str] = None + self.vars = dict() + + async def _send( + self, + message: RequestMessage, + operation: str, + bypass: bool = False, + ) -> Dict[str, Any]: + """ + Sends an HTTP request to the SurrealDB server. + + :param endpoint: (str) The endpoint of the SurrealDB API to send the request to. + :param method: (str) The HTTP method (e.g., "POST", "GET", "PUT", "DELETE"). + :param headers: (dict) Optional headers to include in the request. + :param payload: (dict) Optional JSON payload to include in the request body. + + :return: (dict) The decoded JSON response from the server. + """ + # json_body, method, endpoint = message.JSON_HTTP_DESCRIPTOR + data = message.WS_CBOR_DESCRIPTOR + url = f"{self.url.raw_url}/rpc" + headers = dict() + headers["Accept"] = "application/cbor" + headers["content-type"] = "application/cbor" + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + if self.namespace: + headers["Surreal-NS"] = self.namespace + if self.database: + headers["Surreal-DB"] = self.database + + async with aiohttp.ClientSession() as session: + async with session.request( + method="POST", + url=url, + headers=headers, + # json=json.dumps(json_body), + data=data, + timeout=aiohttp.ClientTimeout(total=30), + ) as response: + response.raise_for_status() + raw_cbor = await response.read() + data = decode(raw_cbor) + if bypass is False: + self.check_response_for_error(data, operation) + return data + + def set_token(self, token: str) -> None: + """ + Sets the token for authentication. + + :param token: (str) The token to use for the connection. + """ + self.token = token + + async def signin(self, vars: dict) -> dict: + message = RequestMessage( + self.id, + RequestMethod.SIGN_IN, + username=vars.get("username"), + password=vars.get("password"), + access=vars.get("access"), + database=vars.get("database"), + namespace=vars.get("namespace"), + variables=vars.get("variables"), + ) + response = await self._send(message, "signing in") + self.check_response_for_result(response, "signing in") + self.token = response["result"] + package = dict() + package["token"] = self.token + return package + + async def use(self, namespace: str, database: str) -> None: + message = RequestMessage( + self.token, + RequestMethod.USE, + namespace=namespace, + database=database, + ) + data = await self._send(message, "use") + self.namespace = namespace + self.database = database + + async def query(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + for key, value in self.vars.items(): + params[key] = value + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = await self._send(message, "query") + self.check_response_for_result(response, "query") + return response["result"][0]["result"] + + async def query_raw(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + for key, value in self.vars.items(): + params[key] = value + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = await self._send(message, "query", bypass=True) + return response + + async def create( + self, + thing: Union[str, RecordID, Table], + data: Optional[Union[Union[List[dict], dict], dict]] = None, + ) -> Union[List[dict], dict]: + if isinstance(thing, str): + if ":" in thing: + buffer = thing.split(":") + thing = RecordID(table_name=buffer[0], identifier=buffer[1]) + message = RequestMessage( + self.id, + RequestMethod.CREATE, + collection=thing, + data=data + ) + response = await self._send(message, "create") + self.check_response_for_result(response, "create") + return response["result"] + + async def delete( + self, thing: Union[str, RecordID, Table] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.DELETE, + record_id=thing + ) + response = await self._send(message, "delete") + self.check_response_for_result(response, "delete") + return response["result"] + + async def info(self) -> dict: + message = RequestMessage( + self.id, + RequestMethod.INFO + ) + response = await self._send(message, "getting database information") + self.check_response_for_result(response, "getting database information") + return response["result"] + + async def insert( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT, + collection=table, + params=data + ) + response = await self._send(message, "insert") + self.check_response_for_result(response, "insert") + return response["result"] + + async def insert_relation( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT_RELATION, + table=table, + params=data + ) + response = await self._send(message, "insert_relation") + self.check_response_for_result(response, "insert_relation") + return response["result"] + + async def invalidate(self) -> None: + message = RequestMessage(self.id, RequestMethod.INVALIDATE) + await self._send(message, "invalidating") + self.token = None + + async def let(self, key: str, value: Any) -> None: + self.vars[key] = value + + async def unset(self, key: str) -> None: + self.vars.pop(key) + + async def merge( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.MERGE, + record_id=thing, + data=data + ) + response = await self._send(message, "merge") + self.check_response_for_result(response, "merge") + return response["result"] + + async def patch( + self, thing: Union[str, RecordID, Table], data: Optional[List[dict]] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.PATCH, + collection=thing, + params=data + ) + response = await self._send(message, "patch") + self.check_response_for_result(response, "patch") + return response["result"] + + async def select(self, thing: str) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.SELECT, + params=[thing] + ) + response = await self._send(message, "select") + self.check_response_for_result(response, "select") + return response["result"] + + async def update( + self, + thing: Union[str, RecordID, Table], + data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPDATE, + record_id=thing, + data=data + ) + response = await self._send(message, "update") + self.check_response_for_result(response, "update") + return response["result"] + + async def version(self) -> str: + message = RequestMessage( + self.id, + RequestMethod.VERSION + ) + response = await self._send(message, "getting database version") + self.check_response_for_result(response, "getting database version") + return response["result"] + + async def upsert( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPSERT, + record_id=thing, + data=data + ) + response = await self._send(message, "upsert") + self.check_response_for_result(response, "upsert") + return response["result"] + + async def signup(self, vars: Dict) -> str: + message = RequestMessage( + self.id, + RequestMethod.SIGN_UP, + data=vars + ) + response = await self._send(message, "signup") + self.check_response_for_result(response, "signup") + self.token = response["result"] + return response["result"] + + async def __aenter__(self) -> "AsyncHttpSurrealConnection": + """ + Asynchronous context manager entry. + Initializes an aiohttp session and returns the connection instance. + """ + self._session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + """ + Asynchronous context manager exit. + Closes the aiohttp session upon exiting the context. + """ + if hasattr(self, "_session"): + await self._session.close() diff --git a/src/surrealdb/connections/async_template.py b/src/surrealdb/connections/async_template.py new file mode 100644 index 00000000..af5e7246 --- /dev/null +++ b/src/surrealdb/connections/async_template.py @@ -0,0 +1,423 @@ +from typing import Optional, List, Dict, Any, Union +from uuid import UUID +from asyncio import Queue +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class AsyncTemplate: + + async def connect(self, url: str) -> None: + """Connects to a local or remote database endpoint. + + Args: + url: The url of the database endpoint to connect to. + options: An object with options to initiate the connection to SurrealDB. + + Example: + # Connect to a remote endpoint + await db.connect('https://cloud.surrealdb.com/rpc'); + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def close(self) -> None: + """Closes the persistent connection to the database. + + Example: + await db.close() + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def use(self, namespace: str, database: str) -> None: + """Switch to a specific namespace and database. + + Args: + namespace: Switches to a specific namespace. + database: Switches to a specific database. + + Example: + await db.use('test', 'test') + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def signup(self, vars: Dict) -> str: + """Sign this connection up to a specific authentication scope. + [See the docs](https://surrealdb.com/docs/sdk/python/methods/signup) + + Args: + vars: Variables used in a signup query. + + Example: + await db.signup({ + namespace: 'surrealdb', + database: 'docs', + access: 'user', + + # Also pass any properties required by the scope definition + variables: { + email: 'info@surrealdb.com', + pass: '123456', + }, + }) + """ + raise NotImplementedError(f"signup not implemented for: {self}") + + async def signin(self, vars: Dict) -> str: + """Sign this connection in to a specific authentication scope. + [See the docs](https://surrealdb.com/docs/sdk/python/methods/signin) + + Args: + vars: Variables used in a signin query. + + Example: + await db.signin({ + username: 'root', + password: 'surrealdb', + }) + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def invalidate(self) -> None: + """Invalidate the authentication for the current connection. + + Example: + await db.invalidate() + """ + raise NotImplementedError(f"invalidate not implemented for: {self}") + + async def authenticate(self, token: str) -> None: + """Authenticate the current connection with a JWT token. + + Args: + token: The JWT authentication token. + + Example: + await db.authenticate('insert token here') + """ + raise NotImplementedError(f"authenticate not implemented for: {self}") + + async def let(self, key: str, value: Any) -> None: + """Assign a value as a variable for this connection. + + Args: + key: Specifies the name of the variable. + value: Assigns the value to the variable name. + + Example: + # Assign the variable on the connection + await db.let('name', { + first: 'Tobie', + last: 'Morgan Hitchcock', + }) + + # Use the variable in a subsequent query + await db.query('CREATE person SET name = $name') + """ + raise NotImplementedError(f"let not implemented for: {self}") + + async def unset(self, key: str) -> None: + """Removes a variable for this connection. + + Args: + key: Specifies the name of the variable. + + Example: + await db.unset('name') + """ + raise NotImplementedError(f"let not implemented for: {self}") + + async def query( + self, query: str, vars: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Run a unset of SurrealQL statements against the database. + + Args: + query: Specifies the SurrealQL statements. + vars: Assigns variables which can be used in the query. + + Example: + await db.query( + 'CREATE person SET name = "John"; SELECT * FROM type::table($tb);', + { tb: 'person' } + ) + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def select(self, thing: Union[str, RecordID, Table]) -> Union[List[dict], dict]: + """Select all records in a table (or other entity), + or a specific record, in the database. + + This function will run the following query in the database: + `select * from $thing` + + Args: + thing: The table or record ID to select. + + Example: + db.select('person') + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def create( + self, + thing: Union[str, RecordID, Table], + data: Optional[Union[Union[List[dict], dict], dict]] = None, + ) -> Union[List[dict], dict]: + """Create a record in the database. + + This function will run the following query in the database: + `create $thing content $data` + + Args: + thing: The table or record ID. + data (optional): The document / record data to insert. + + Example: + db.create + """ + raise NotImplementedError(f"create not implemented for: {self}") + + async def update( + self, + thing: Union[str, RecordID, Table], + data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Update all records in a table, or a specific record, in the database. + + This function replaces the current document / record data with the + specified data. + + This function will run the following query in the database: + `update $thing content $data` + + Args: + thing: The table or record ID. + data (optional): The document / record data to insert. + + Example: + Update all records in a table + person = await db.update('person') + + Update a record with a specific ID + record = await db.update('person:tobie', { + 'name': 'Tobie', + 'settings': { + 'active': true, + 'marketing': true, + }, + }) + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def upsert( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Insert records into the database, or to update them if they exist. + + + This function will run the following query in the database: + `upsert $thing content $data` + + Args: + thing: The table or record ID. + data (optional): The document / record data to insert. + + Example: + Insert or update all records in a table + person = await db.upsert('person') + + Insert or update a record with a specific ID + record = await db.upsert('person:tobie', { + 'name': 'Tobie', + 'settings': { + 'active': true, + 'marketing': true, + }, + }) + """ + raise NotImplementedError(f"upsert not implemented for: {self}") + + async def merge( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Modify by deep merging all records in a table, or a specific record, in the database. + + This function merges the current document / record data with the + specified data. + + This function will run the following query in the database: + `update $thing merge $data` + + Args: + thing: The table name or the specific record ID to change. + data (optional): The document / record data to insert. + + Example: + Update all records in a table + people = await db.merge('person', { + 'updated_at': str(datetime.datetime.utcnow()) + }) + + Update a record with a specific ID + person = await db.merge('person:tobie', { + 'updated_at': str(datetime.datetime.utcnow()), + 'settings': { + 'active': True, + }, + }) + + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def patch( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Apply JSON Patch changes to all records, or a specific record, in the database. + + This function patches the current document / record data with + the specified JSON Patch data. + + This function will run the following query in the database: + `update $thing patch $data` + + Args: + thing: The table or record ID. + data: The data to modify the record with. + + Example: + Update all records in a table + people = await db.patch('person', [ + { 'op': "replace", 'path': "/created_at", 'value': str(datetime.datetime.utcnow()) }]) + + Update a record with a specific ID + person = await db.patch('person:tobie', [ + { 'op': "replace", 'path': "/settings/active", 'value': False }, + { 'op': "add", "path": "/tags", "value": ["developer", "engineer"] }, + { 'op': "remove", "path": "/temp" }, + ]) + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def delete( + self, thing: Union[str, RecordID, Table] + ) -> Union[List[dict], dict]: + """Delete all records in a table, or a specific record, from the database. + + This function will run the following query in the database: + `delete $thing` + + Args: + thing: The table name or a RecordID to delete. + + Example: + Delete a specific record from a table + await db.delete(RecordID('person', 'h5wxrf2ewk8xjxosxtyc')) + + Delete all records from a table + await db.delete('person') + """ + raise NotImplementedError(f"delete not implemented for: {self}") + + def info(self) -> dict: + """This returns the record of an authenticated record user. + + Example: + await db.info() + """ + raise NotImplementedError(f"query not implemented for: {self}") + + def insert( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + """ + Inserts one or multiple records in the database. + + This function will run the following query in the database: + `INSERT INTO $thing $data` + + Args: + table: The table name to insert records in to + data: Either a single document/record or an array of documents/records to insert + + Example: + await db.insert('person', [{ name: 'Tobie'}, { name: 'Jaime'}]) + + """ + raise NotImplementedError(f"query not implemented for: {self}") + + def insert_relation( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + """ + Inserts one or multiple relations in the database. + + This function will run the following query in the database: + `INSERT RELATION INTO $table $data` + + Args: + table: The table name to insert records in to + data: Either a single document/record or an array of documents/records to insert + + Example: + await db.insert_relation('likes', { in: person:1, id: 'object', out: person:2}) + + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def live(self, table: Union[str, Table], diff: bool = False) -> UUID: + """Initiates a live query for a specified table name. + + Args: + table: The table name to listen for changes for. + diff: If unset to true, live notifications will include + an array of JSON Patch objects, rather than + the entire record for each notification. Defaults to false. + + Returns: + The live query uuid + + Example: + await db.live('person') + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def subscribe_live(self, query_uuid: Union[str, UUID]) -> Queue: + """Returns a queue that receives notification messages from a running live query. + + Args: + query_uuid: The uuid for the live query + + Returns: + the notification queue + + Example: + await db.subscribe_live(UUID) + """ + raise NotImplementedError(f"query not implemented for: {self}") + + async def kill(self, query_uuid: Union[str, UUID]) -> None: + """Kills a running live query by it's UUID. + + Args: + query_uuid: The UUID of the live query you wish to kill. + + Example: + await db.kill(UUID) + + """ + raise NotImplementedError(f"query not implemented for: {self}") + + + async def signin(self, vars: Dict) -> str: + """Sign this connection in to a specific authentication scope. + [See the docs](https://surrealdb.com/docs/sdk/python/methods/signin) + + Args: + vars: Variables used in a signin query. + + Example: + await db.signin({ + username: 'root', + password: 'surrealdb', + }) + """ \ No newline at end of file diff --git a/src/surrealdb/connections/async_ws.py b/src/surrealdb/connections/async_ws.py new file mode 100644 index 00000000..ca1b84f1 --- /dev/null +++ b/src/surrealdb/connections/async_ws.py @@ -0,0 +1,378 @@ +""" +A basic async connection to a SurrealDB instance. +""" +import asyncio +import uuid +from asyncio import Queue +from typing import Optional, Any, Dict, Union, List, AsyncGenerator +from uuid import UUID + +import websockets + +from surrealdb.connections.async_template import AsyncTemplate +from surrealdb.connections.url import Url +from surrealdb.connections.utils_mixin import UtilsMixin +from surrealdb.data.cbor import decode +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class AsyncWsSurrealConnection(AsyncTemplate, UtilsMixin): + """ + A single async connection to a SurrealDB instance. To be used once and discarded. + + # Notes + A new connection is created for each query. This is because the async websocket connection is + dropped + + Attributes: + url: The URL of the database to process queries for. + user: The username to login on. + password: The password to login on. + namespace: The namespace that the connection will stick to. + database: The database that the connection will stick to. + max_size: The maximum size of the connection. + id: The ID of the connection. + """ + def __init__( + self, + url: str, + max_size: int = 2 ** 20, + ) -> None: + """ + The constructor for the AsyncSurrealConnection class. + + :param url: The URL of the database to process queries for. + :param max_size: The maximum size of the connection. + """ + self.url: Url = Url(url) + self.raw_url: str = f"{self.url.raw_url}/rpc" + self.host: str = self.url.hostname + self.port: int = self.url.port + self.max_size: int = max_size + self.id: str = str(uuid.uuid4()) + self.token: Optional[str] = None + self.socket = None + + async def _send(self, message: RequestMessage, process: str, bypass: bool = False) -> dict: + await self.connect() + await self.socket.send(message.WS_CBOR_DESCRIPTOR) + response = decode(await self.socket.recv()) + if bypass is False: + self.check_response_for_error(response, process) + return response + + async def connect(self, url: Optional[str] = None, max_size: Optional[int] = None) -> None: + # overwrite params if passed in + if url is not None: + self.url = Url(url) + self.raw_url: str = f"{self.url.raw_url}/rpc" + self.host: str = self.url.hostname + self.port: int = self.url.port + if max_size is not None: + self.max_size = max_size + if self.socket is None: + self.socket = await websockets.connect( + self.raw_url, + max_size=self.max_size, + subprotocols=[websockets.Subprotocol("cbor")] + ) + + # async def signup(self, vars: Dict[str, Any]) -> str: + + async def signin(self, vars: Dict[str, Any]) -> str: + message = RequestMessage( + self.id, + RequestMethod.SIGN_IN, + username=vars.get("username"), + password=vars.get("password"), + access=vars.get("access"), + database=vars.get("database"), + namespace=vars.get("namespace"), + variables=vars.get("variables"), + ) + response = await self._send(message, "signing in") + self.check_response_for_result(response, "signing in") + self.token = response["result"] + if response.get("id") is None: + raise Exception(f"no id signing in: {response}") + self.id = response["id"] + + async def query(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = await self._send(message, "query") + self.check_response_for_result(response, "query") + return response["result"][0]["result"] + + async def query_raw(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = await self._send(message, "query", bypass=True) + return response + + async def use(self, namespace: str, database: str) -> None: + message = RequestMessage( + self.id, + RequestMethod.USE, + namespace=namespace, + database=database, + ) + await self._send(message, "use") + + async def info(self) -> Optional[dict]: + message = RequestMessage( + self.id, + RequestMethod.INFO + ) + outcome = await self._send(message, "getting database information") + self.check_response_for_result(outcome, "getting database information") + return outcome["result"] + + async def version(self) -> str: + message = RequestMessage( + self.id, + RequestMethod.VERSION + ) + response = await self._send(message, "getting database version") + self.check_response_for_result(response, "getting database version") + return response["result"] + + async def authenticate(self, token: str) -> dict: + message = RequestMessage( + self.id, + RequestMethod.AUTHENTICATE, + token=token + ) + return await self._send(message, "authenticating") + + async def invalidate(self) -> None: + message = RequestMessage(self.id, RequestMethod.INVALIDATE) + await self._send(message, "invalidating") + + async def let(self, key: str, value: Any) -> None: + message = RequestMessage( + self.id, + RequestMethod.LET, + key=key, + value=value + ) + await self._send(message, "letting") + + async def unset(self, key: str) -> None: + message = RequestMessage( + self.id, + RequestMethod.UNSET, + params=[key] + ) + await self._send(message, "unsetting") + + async def select(self, thing: str) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.SELECT, + params=[thing] + ) + response = await self._send(message, "select") + self.check_response_for_result(response, "select") + return response["result"] + + async def create( + self, + thing: Union[str, RecordID, Table], + data: Optional[Union[Union[List[dict], dict], dict]] = None, + ) -> Union[List[dict], dict]: + if isinstance(thing, str): + if ":" in thing: + buffer = thing.split(":") + thing = RecordID(table_name=buffer[0], identifier=buffer[1]) + message = RequestMessage( + self.id, + RequestMethod.CREATE, + collection=thing, + data=data + ) + response = await self._send(message, "create") + self.check_response_for_result(response, "create") + return response["result"] + + async def update( + self, + thing: Union[str, RecordID, Table], + data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPDATE, + record_id=thing, + data=data + ) + response = await self._send(message, "update") + self.check_response_for_result(response, "update") + return response["result"] + + async def merge( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.MERGE, + record_id=thing, + data=data + ) + response = await self._send(message, "merge") + self.check_response_for_result(response, "merge") + return response["result"] + + async def patch( + self, thing: Union[str, RecordID, Table], data: Optional[List[dict]] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.PATCH, + collection=thing, + params=data + ) + response = await self._send(message, "patch") + self.check_response_for_result(response, "patch") + return response["result"] + + async def delete( + self, thing: Union[str, RecordID, Table] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.DELETE, + record_id=thing + ) + response = await self._send(message, "delete") + self.check_response_for_result(response, "delete") + return response["result"] + + async def insert( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT, + collection=table, + params=data + ) + response = await self._send(message, "insert") + self.check_response_for_result(response, "insert") + return response["result"] + + async def insert_relation( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT_RELATION, + table=table, + params=data + ) + response = await self._send(message, "insert_relation") + self.check_response_for_result(response, "insert_relation") + return response["result"] + + async def live(self, table: Union[str, Table], diff: bool = False) -> UUID: + message = RequestMessage( + self.id, + RequestMethod.LIVE, + table=table, + ) + response = await self._send(message, "live") + self.check_response_for_result(response, "live") + return response["result"] + + async def subscribe_live(self, query_uuid: Union[str, UUID]) -> AsyncGenerator[dict, None]: + result_queue = Queue() + + async def listen_live(): + """ + Listen for live updates from the WebSocket and put them into the queue. + """ + try: + while True: + response = decode(await self.socket.recv()) + if response.get("result", {}).get("id") == query_uuid: + await result_queue.put(response["result"]["result"]) + except Exception as e: + print("Error in live subscription:", e) + await result_queue.put({"error": str(e)}) + + asyncio.create_task(listen_live()) + + while True: + result = await result_queue.get() + if "error" in result: + raise Exception(f"Error in live subscription: {result['error']}") + yield result + + async def kill(self, query_uuid: Union[str, UUID]) -> None: + message = RequestMessage( + self.id, + RequestMethod.KILL, + uuid=query_uuid + ) + await self._send(message, "kill") + + async def signup(self, vars: Dict) -> str: + message = RequestMessage( + self.id, + RequestMethod.SIGN_UP, + data=vars + ) + response = await self._send(message, "signup") + self.check_response_for_result(response, "signup") + return response["result"] + + async def upsert( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPSERT, + record_id=thing, + data=data + ) + response = await self._send(message, "upsert") + self.check_response_for_result(response, "upsert") + return response["result"] + + def close(self): + self.socket.close() + + async def __aenter__(self) -> "AsyncWsSurrealConnection": + """ + Asynchronous context manager entry. + Initializes a websocket connection and returns the connection instance. + """ + self.socket = await websockets.connect( + self.raw_url, + max_size=self.max_size, + subprotocols=[websockets.Subprotocol("cbor")] + ) + return self + + async def __aexit__(self, exc_type, exc_value, traceback) -> None: + """ + Asynchronous context manager exit. + Closes the websocket connection upon exiting the context. + """ + if self.socket is not None: + await self.socket.close() diff --git a/src/surrealdb/connections/blocking_http.py b/src/surrealdb/connections/blocking_http.py new file mode 100644 index 00000000..89ebd6cd --- /dev/null +++ b/src/surrealdb/connections/blocking_http.py @@ -0,0 +1,287 @@ +import uuid +from typing import Optional, Any, Dict, Union, List + +import requests + +from surrealdb.connections.sync_template import SyncTemplate +from surrealdb.connections.url import Url +from surrealdb.connections.utils_mixin import UtilsMixin +from surrealdb.data.cbor import decode +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class BlockingHttpSurrealConnection(SyncTemplate, UtilsMixin): + + def __init__(self, url: str) -> None: + self.url: Url = Url(url) + self.raw_url: str = url.rstrip("/") + self.host: str = self.url.hostname + self.port: Optional[int] = self.url.port + self.token: Optional[str] = None + self.id: str = str(uuid.uuid4()) + self.namespace: Optional[str] = None + self.database: Optional[str] = None + self.vars = dict() + + def _send(self, message: RequestMessage, operation: str, bypass: bool = False) -> Dict[str, Any]: + data = message.WS_CBOR_DESCRIPTOR + url = f"{self.url.raw_url}/rpc" + headers = { + "Accept": "application/cbor", + "Content-Type": "application/cbor", + } + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + if self.namespace: + headers["Surreal-NS"] = self.namespace + if self.database: + headers["Surreal-DB"] = self.database + + response = requests.post(url, headers=headers, data=data, timeout=30) + response.raise_for_status() + raw_cbor = response.content + data = decode(raw_cbor) + if bypass is False: + self.check_response_for_error(data, operation) + return data + + def set_token(self, token: str) -> None: + self.token = token + + def signin(self, vars: dict) -> dict: + message = RequestMessage( + self.id, + RequestMethod.SIGN_IN, + username=vars.get("username"), + password=vars.get("password"), + access=vars.get("access"), + database=vars.get("database"), + namespace=vars.get("namespace"), + variables=vars.get("variables"), + ) + response = self._send(message, "signing in") + self.check_response_for_result(response, "signing in") + self.token = response["result"] + package = dict() + package["token"] = self.token + return package + + def use(self, namespace: str, database: str) -> None: + message = RequestMessage( + self.token, + RequestMethod.USE, + namespace=namespace, + database=database, + ) + data = self._send(message, "use") + self.namespace = namespace + self.database = database + + def query(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + for key, value in self.vars.items(): + params[key] = value + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = self._send(message, "query") + self.check_response_for_result(response, "query") + return response["result"][0]["result"] + + def query_raw(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + for key, value in self.vars.items(): + params[key] = value + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = self._send(message, "query", bypass=True) + return response + + def create( + self, + thing: Union[str, RecordID, Table], + data: Optional[Union[Union[List[dict], dict], dict]] = None, + ) -> Union[List[dict], dict]: + if isinstance(thing, str): + if ":" in thing: + buffer = thing.split(":") + thing = RecordID(table_name=buffer[0], identifier=buffer[1]) + message = RequestMessage( + self.id, + RequestMethod.CREATE, + collection=thing, + data=data + ) + response = self._send(message, "create") + self.check_response_for_result(response, "create") + return response["result"] + + def delete( + self, thing: Union[str, RecordID, Table] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.DELETE, + record_id=thing + ) + response = self._send(message, "delete") + self.check_response_for_result(response, "delete") + return response["result"] + + def info(self): + message = RequestMessage( + self.id, + RequestMethod.INFO + ) + response = self._send(message, "getting database information") + self.check_response_for_result(response, "getting database information") + return response["result"] + + def insert( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT, + collection=table, + params=data + ) + response = self._send(message, "insert") + self.check_response_for_result(response, "insert") + return response["result"] + + def insert_relation( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT_RELATION, + table=table, + params=data + ) + response = self._send(message, "insert_relation") + self.check_response_for_result(response, "insert_relation") + return response["result"] + + def invalidate(self) -> None: + message = RequestMessage(self.id, RequestMethod.INVALIDATE) + self._send(message, "invalidating") + self.token = None + + def let(self, key: str, value: Any) -> None: + self.vars[key] = value + + def unset(self, key: str) -> None: + self.vars.pop(key) + + def merge( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.MERGE, + record_id=thing, + data=data + ) + response = self._send(message, "merge") + self.check_response_for_result(response, "merge") + return response["result"] + + def patch( + self, thing: Union[str, RecordID, Table], data: Optional[List[dict]] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.PATCH, + collection=thing, + params=data + ) + response = self._send(message, "patch") + self.check_response_for_result(response, "patch") + return response["result"] + + def select(self, thing: str) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.SELECT, + params=[thing] + ) + response = self._send(message, "select") + self.check_response_for_result(response, "select") + return response["result"] + + def update( + self, + thing: Union[str, RecordID, Table], + data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPDATE, + record_id=thing, + data=data + ) + response = self._send(message, "update") + self.check_response_for_result(response, "update") + return response["result"] + + def version(self) -> str: + message = RequestMessage( + self.id, + RequestMethod.VERSION + ) + response = self._send(message, "getting database version") + self.check_response_for_result(response, "getting database version") + return response["result"] + + def upsert( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPSERT, + record_id=thing, + data=data + ) + response = self._send(message, "upsert") + self.check_response_for_result(response, "upsert") + return response["result"] + + def signup(self, vars: Dict) -> str: + message = RequestMessage( + self.id, + RequestMethod.SIGN_UP, + data=vars + ) + response = self._send(message, "signup") + self.check_response_for_result(response, "signup") + self.token = response["result"] + return response["result"] + + def __enter__(self) -> "BlockingHttpSurrealConnection": + """ + Synchronous context manager entry. + Initializes a session for HTTP requests. + """ + self.session = requests.Session() + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + """ + Synchronous context manager exit. + Closes the HTTP session upon exiting the context. + """ + if hasattr(self, "session"): + self.session.close() diff --git a/src/surrealdb/connections/blocking_ws.py b/src/surrealdb/connections/blocking_ws.py new file mode 100644 index 00000000..fc15580b --- /dev/null +++ b/src/surrealdb/connections/blocking_ws.py @@ -0,0 +1,367 @@ +""" +A basic blocking connection to a SurrealDB instance. +""" +import uuid +from typing import Optional, Any, Dict, Union, List, Generator +from uuid import UUID + +import websockets +import websockets.sync.client as ws_sync + +from surrealdb.connections.sync_template import SyncTemplate +from surrealdb.connections.url import Url +from surrealdb.connections.utils_mixin import UtilsMixin +from surrealdb.data.cbor import decode +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class BlockingWsSurrealConnection(SyncTemplate, UtilsMixin): + """ + A single blocking connection to a SurrealDB instance. To be used once and discarded. + + # Notes + A new connection is created for each query. This is because the WebSocket connection is + dropped after the query is completed. + + Attributes: + url: The URL of the database to process queries for. + user: The username to login on. + password: The password to login on. + namespace: The namespace that the connection will stick to. + database: The database that the connection will stick to. + max_size: The maximum size of the connection. + id: The ID of the connection. + """ + + def __init__(self, url: str, max_size: int = 2 ** 20) -> None: + """ + The constructor for the BlockingWsSurrealConnection class. + + :param url: (str) the URL of the database to process queries for. + :param max_size: (int) The maximum size of the connection. + """ + self.url: Url = Url(url) + self.raw_url: str = f"{self.url.raw_url}/rpc" + self.host: str = self.url.hostname + self.port: int = self.url.port + self.max_size: int = max_size + self.id: str = str(uuid.uuid4()) + self.token: Optional[str] = None + self.socket = None + + def _send(self, message: RequestMessage, process: str, bypass: bool = False) -> dict: + if self.socket is None: + self.socket = ws_sync.connect( + self.raw_url, + max_size=self.max_size, + subprotocols=[websockets.Subprotocol("cbor")], + ) + self.socket.send(message.WS_CBOR_DESCRIPTOR) + response = decode(self.socket.recv()) + if bypass is False: + self.check_response_for_error(response, process) + return response + + def signin(self, vars: Dict[str, Any]) -> str: + message = RequestMessage( + self.id, + RequestMethod.SIGN_IN, + username=vars.get("username"), + password=vars.get("password"), + access=vars.get("access"), + database=vars.get("database"), + namespace=vars.get("namespace"), + variables=vars.get("variables"), + ) + response = self._send(message, "signing in") + self.check_response_for_result(response, "signing in") + self.token = response["result"] + if response.get("id") is None: + raise Exception(f"No ID signing in: {response}") + self.id = response["id"] + + def query(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = self._send(message, "query") + self.check_response_for_result(response, "query") + return response["result"][0]["result"] + + def query_raw(self, query: str, params: Optional[dict] = None) -> dict: + if params is None: + params = {} + message = RequestMessage( + self.id, + RequestMethod.QUERY, + query=query, + params=params, + ) + response = self._send(message, "query", bypass=True) + return response + + def use(self, namespace: str, database: str) -> None: + message = RequestMessage( + self.id, + RequestMethod.USE, + namespace=namespace, + database=database, + ) + self._send(message, "use") + + def info(self) -> dict: + message = RequestMessage( + self.id, + RequestMethod.INFO + ) + response = self._send(message, "getting database information") + self.check_response_for_result(response, "getting database information") + return response["result"] + + def version(self) -> str: + message = RequestMessage( + self.id, + RequestMethod.VERSION + ) + response = self._send(message, "getting database version") + self.check_response_for_result(response, "getting database version") + return response["result"] + + def authenticate(self, token: str) -> dict: + message = RequestMessage( + self.id, + RequestMethod.AUTHENTICATE, + token=token + ) + return self._send(message, "authenticating") + + def invalidate(self) -> None: + message = RequestMessage(self.id, RequestMethod.INVALIDATE) + self._send(message, "invalidating") + + def let(self, key: str, value: Any) -> None: + message = RequestMessage( + self.id, + RequestMethod.LET, + key=key, + value=value + ) + self._send(message, "letting") + + def unset(self, key: str) -> None: + message = RequestMessage( + self.id, + RequestMethod.UNSET, + params=[key] + ) + self._send(message, "unsetting") + + def select(self, thing: str) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.SELECT, + params=[thing] + ) + response = self._send(message, "select") + self.check_response_for_result(response, "select") + return response["result"] + + def create( + self, + thing: Union[str, RecordID, Table], + data: Optional[Union[Union[List[dict], dict], dict]] = None, + ) -> Union[List[dict], dict]: + if isinstance(thing, str): + if ":" in thing: + buffer = thing.split(":") + thing = RecordID(table_name=buffer[0], identifier=buffer[1]) + message = RequestMessage( + self.id, + RequestMethod.CREATE, + collection=thing, + data=data + ) + response = self._send(message, "create") + self.check_response_for_result(response, "create") + return response["result"] + + def live(self, table: Union[str, Table], diff: bool = False) -> UUID: + message = RequestMessage( + self.id, + RequestMethod.LIVE, + table=table, + ) + response = self._send(message, "live") + self.check_response_for_result(response, "live") + return response["result"] + + def kill(self, query_uuid: Union[str, UUID]) -> None: + message = RequestMessage( + self.id, + RequestMethod.KILL, + uuid=query_uuid + ) + self._send(message, "kill") + + def delete( + self, thing: Union[str, RecordID, Table] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.DELETE, + record_id=thing + ) + response = self._send(message, "delete") + self.check_response_for_result(response, "delete") + return response["result"] + + def insert( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT, + collection=table, + params=data + ) + response = self._send(message, "insert") + self.check_response_for_result(response, "insert") + return response["result"] + + def insert_relation( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.INSERT_RELATION, + table=table, + params=data + ) + response = self._send(message, "insert_relation") + self.check_response_for_result(response, "insert_relation") + return response["result"] + + def merge( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.MERGE, + record_id=thing, + data=data + ) + response = self._send(message, "merge") + self.check_response_for_result(response, "merge") + return response["result"] + + def patch( + self, thing: Union[str, RecordID, Table], data: Optional[List[dict]] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.PATCH, + collection=thing, + params=data + ) + response = self._send(message, "patch") + self.check_response_for_result(response, "patch") + return response["result"] + + def subscribe_live(self, query_uuid: Union[str, UUID]) -> Generator[dict, None, None]: + """ + Subscribe to live updates for a given query UUID. + + Args: + query_uuid (Union[str, UUID]): The query UUID to subscribe to. + + Yields: + dict: The results of live updates. + """ + try: + while True: + try: + # Receive a message from the WebSocket + response = decode(self.socket.recv()) + + # Check if the response matches the query UUID + if response.get("result", {}).get("id") == query_uuid: + yield response["result"]["result"] + except Exception as e: + # Handle WebSocket or decoding errors + print("Error in live subscription:", e) + yield {"error": str(e)} + except GeneratorExit: + # Handle generator exit gracefully if needed + pass + + def update( + self, + thing: Union[str, RecordID, Table], + data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPDATE, + record_id=thing, + data=data + ) + response = self._send(message, "update") + self.check_response_for_result(response, "update") + return response["result"] + + def upsert( + self, + thing: Union[str, RecordID, Table], + data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + message = RequestMessage( + self.id, + RequestMethod.UPSERT, + record_id=thing, + data=data + ) + response = self._send(message, "upsert") + self.check_response_for_result(response, "upsert") + return response["result"] + + def signup(self, vars: Dict) -> str: + message = RequestMessage( + self.id, + RequestMethod.SIGN_UP, + data=vars + ) + response = self._send(message, "signup") + self.check_response_for_result(response, "signup") + return response["result"] + + def close(self): + self.socket.close() + + def __enter__(self) -> "BlockingWsSurrealConnection": + """ + Synchronous context manager entry. + Initializes a websocket connection and returns the connection instance. + """ + self.socket = ws_sync.connect( + self.raw_url, + max_size=self.max_size, + subprotocols=[websockets.Subprotocol("cbor")] + ) + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + """ + Synchronous context manager exit. + Closes the websocket connection upon exiting the context. + """ + if self.socket is not None: + self.socket.close() + diff --git a/src/surrealdb/connections/sync_template.py b/src/surrealdb/connections/sync_template.py new file mode 100644 index 00000000..0decebb5 --- /dev/null +++ b/src/surrealdb/connections/sync_template.py @@ -0,0 +1,428 @@ +from typing import Optional, List, Dict, Any, Union +from uuid import UUID +from asyncio import Queue +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class SyncTemplate: + + # def connect(self, url: str, options: Optional[Dict] = None) -> None: + # """Connects to a local or remote database endpoint. + # + # Args: + # url: The url of the database endpoint to connect to. + # options: An object with options to initiate the connection to SurrealDB. + # + # Example: + # # Connect to a remote endpoint + # db.connect('https://cloud.surrealdb.com/rpc'); + # + # # Specify a namespace and database pair to use + # db.connect('https://cloud.surrealdb.com/rpc', { + # namespace: 'surrealdb', + # database: 'docs', + # }); + # """ + # raise NotImplementedError(f"query not implemented for: {self}") + + def close(self) -> None: + """Closes the persistent connection to the database. + + Example: + db.close() + """ + raise NotImplementedError(f"close not implemented for: {self}") + + def use(self, namespace: str, database: str) -> None: + """Switch to a specific namespace and database. + + Args: + namespace: Switches to a specific namespace. + database: Switches to a specific database. + + Example: + db.use('test', 'test') + """ + raise NotImplementedError(f"use not implemented for: {self}") + + def signup(self, vars: Dict) -> str: + """Sign this connection up to a specific authentication scope. + [See the docs](https://surrealdb.com/docs/sdk/python/methods/signup) + + Args: + vars: Variables used in a signup query. + + Example: + db.signup({ + namespace: 'surrealdb', + database: 'docs', + access: 'user', + + # Also pass any properties required by the scope definition + variables: { + email: 'info@surrealdb.com', + pass: '123456', + }, + }) + """ + raise NotImplementedError(f"signup not implemented for: {self}") + + def signin(self, vars: Dict) -> str: + """Sign this connection in to a specific authentication scope. + [See the docs](https://surrealdb.com/docs/sdk/python/methods/signin) + + Args: + vars: Variables used in a signin query. + + Example: + db.signin({ + username: 'root', + password: 'surrealdb', + }) + """ + raise NotImplementedError(f"signin not implemented for: {self}") + + def invalidate(self) -> None: + """Invalidate the authentication for the current connection. + + Example: + db.invalidate() + """ + raise NotImplementedError(f"invalidate not implemented for: {self}") + + def authenticate(self, token: str) -> None: + """Authenticate the current connection with a JWT token. + + Args: + token: The JWT authentication token. + + Example: + db.authenticate('insert token here') + """ + raise NotImplementedError(f"authenticate not implemented for: {self}") + + def let(self, key: str, value: Any) -> None: + """Assign a value as a variable for this connection. + + Args: + key: Specifies the name of the variable. + value: Assigns the value to the variable name. + + Example: + # Assign the variable on the connection + db.let('name', { + first: 'Tobie', + last: 'Morgan Hitchcock', + }) + + # Use the variable in a subsequent query + db.query('CREATE person SET name = $name') + """ + raise NotImplementedError(f"let not implemented for: {self}") + + def unset(self, key: str) -> None: + """Removes a variable for this connection. + + Args: + key: Specifies the name of the variable. + + Example: + db.unset('name') + """ + raise NotImplementedError(f"let not implemented for: {self}") + + def query( + self, query: str, vars: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Run a set of SurrealQL statements against the database. + + Args: + query: Specifies the SurrealQL statements. + vars: Assigns variables which can be used in the query. + + Example: + db.query( + 'CREATE person SET name = "John"; SELECT * FROM type::table($tb);', + { tb: 'person' } + ) + """ + raise NotImplementedError(f"query not implemented for: {self}") + + def select(self, thing: Union[str, RecordID, Table]) -> Union[List[dict], dict]: + """Select all records in a table (or other entity), + or a specific record, in the database. + + This function will run the following query in the database: + `select * from $thing` + + Args: + thing: The table or record ID to select. + + Example: + db.select('person') + """ + raise NotImplementedError(f"select not implemented for: {self}") + + def create( + self, + thing: Union[str, RecordID, Table], + data: Optional[Union[Union[List[dict], dict], dict]] = None, + ) -> Union[List[dict], dict]: + """Create a record in the database. + + This function will run the following query in the database: + `create $thing content $data` + + Args: + thing: The table or record ID. + data (optional): The document / record data to insert. + + Example: + db.create + """ + raise NotImplementedError(f"create not implemented for: {self}") + + def update( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Update all records in a table, or a specific record, in the database. + + This function replaces the current document / record data with the + specified data. + + This function will run the following query in the database: + `update $thing content $data` + + Args: + thing: The table or record ID. + data (optional): The document / record data to insert. + + Example: + Update all records in a table + person = db.update('person') + + Update a record with a specific ID + record = db.update('person:tobie', { + 'name': 'Tobie', + 'settings': { + 'active': true, + 'marketing': true, + }, + }) + """ + raise NotImplementedError(f"update not implemented for: {self}") + + def upsert( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Insert records into the database, or to update them if they exist. + + + This function will run the following query in the database: + `upsert $thing content $data` + + Args: + thing: The table or record ID. + data (optional): The document / record data to insert. + + Example: + Insert or update all records in a table + person = db.upsert('person') + + Insert or update a record with a specific ID + record = db.upsert('person:tobie', { + 'name': 'Tobie', + 'settings': { + 'active': true, + 'marketing': true, + }, + }) + """ + raise NotImplementedError(f"upsert not implemented for: {self}") + + def merge( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Modify by deep merging all records in a table, or a specific record, in the database. + + This function merges the current document / record data with the + specified data. + + This function will run the following query in the database: + `update $thing merge $data` + + Args: + thing: The table name or the specific record ID to change. + data (optional): The document / record data to insert. + + Example: + Update all records in a table + people = db.merge('person', { + 'updated_at': str(datetime.datetime.utcnow()) + }) + + Update a record with a specific ID + person = db.merge('person:tobie', { + 'updated_at': str(datetime.datetime.utcnow()), + 'settings': { + 'active': True, + }, + }) + + """ + raise NotImplementedError(f"merge not implemented for: {self}") + + def patch( + self, thing: Union[str, RecordID, Table], data: Optional[Dict] = None + ) -> Union[List[dict], dict]: + """Apply JSON Patch changes to all records, or a specific record, in the database. + + This function patches the current document / record data with + the specified JSON Patch data. + + This function will run the following query in the database: + `update $thing patch $data` + + Args: + thing: The table or record ID. + data: The data to modify the record with. + + Example: + Update all records in a table + people = db.patch('person', [ + { 'op': "replace", 'path': "/created_at", 'value': str(datetime.datetime.utcnow()) }]) + + Update a record with a specific ID + person = db.patch('person:tobie', [ + { 'op': "replace", 'path': "/settings/active", 'value': False }, + { 'op': "add", "path": "/tags", "value": ["developer", "engineer"] }, + { 'op': "remove", "path": "/temp" }, + ]) + """ + raise NotImplementedError(f"patch not implemented for: {self}") + + def delete( + self, thing: Union[str, RecordID, Table] + ) -> Union[List[dict], dict]: + """Delete all records in a table, or a specific record, from the database. + + This function will run the following query in the database: + `delete $thing` + + Args: + thing: The table name or a RecordID to delete. + + Example: + Delete a specific record from a table + db.delete(RecordID('person', 'h5wxrf2ewk8xjxosxtyc')) + + Delete all records from a table + db.delete('person') + """ + raise NotImplementedError(f"delete not implemented for: {self}") + + def info(self) -> dict: + """This returns the record of an authenticated record user. + + Example: + db.info() + """ + raise NotImplementedError(f"info not implemented for: {self}") + + def insert( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + """ + Inserts one or multiple records in the database. + + This function will run the following query in the database: + `INSERT INTO $thing $data` + + Args: + table: The table name to insert records in to + data: Either a single document/record or an array of documents/records to insert + + Example: + db.insert('person', [{ name: 'Tobie'}, { name: 'Jaime'}]) + + """ + raise NotImplementedError(f"insert not implemented for: {self}") + + def insert_relation( + self, table: Union[str, Table], data: Union[List[dict], dict] + ) -> Union[List[dict], dict]: + """ + Inserts one or multiple relations in the database. + + This function will run the following query in the database: + `INSERT RELATION INTO $table $data` + + Args: + table: The table name to insert records in to + data: Either a single document/record or an array of documents/records to insert + + Example: + db.insert_relation('likes', { in: person:1, id: 'object', out: person:2}) + + """ + raise NotImplementedError(f"insert_relation not implemented for: {self}") + + def live(self, table: Union[str, Table], diff: bool = False) -> UUID: + """Initiates a live query for a specified table name. + + Args: + table: The table name to listen for changes for. + diff: If set to true, live notifications will include + an array of JSON Patch objects, rather than + the entire record for each notification. Defaults to false. + + Returns: + The live query uuid + + Example: + db.live('person') + """ + raise NotImplementedError(f"live not implemented for: {self}") + + def subscribe_live(self, query_uuid: Union[str, UUID]) -> Queue: + """Live notification returns a queue that receives notification messages from the back end. + + Args: + query_uuid: The uuid for the live query + + Returns: + the notification queue + + Example: + db.subscribe_live(UUID) + """ + raise NotImplementedError(f"subscribe_live not implemented for: {self}") + + def kill(self, query_uuid: Union[str, UUID]) -> None: + """Kills a running live query by it's UUID. + + Args: + query_uuid: The UUID of the live query you wish to kill. + + Example: + db.kill(UUID) + + """ + raise NotImplementedError(f"kill not implemented for: {self}") + + + def signin(self, vars: Dict) -> str: + """Sign this connection in to a specific authentication scope. + [See the docs](https://surrealdb.com/docs/sdk/python/methods/signin) + + Args: + vars: Variables used in a signin query. + + Example: + db.signin({ + username: 'root', + password: 'surrealdb', + }) + """ + raise NotImplementedError(f"signin not implemented for: {self}") \ No newline at end of file diff --git a/src/surrealdb/connections/url.py b/src/surrealdb/connections/url.py new file mode 100644 index 00000000..ad23133e --- /dev/null +++ b/src/surrealdb/connections/url.py @@ -0,0 +1,20 @@ +from urllib.parse import urlparse +from enum import Enum + + +class UrlScheme(Enum): + HTTP = "http" + HTTPS = "https" + WS = "ws" + WSS = "wss" + MEM = "mem" + + +class Url: + + def __init__(self, url: str) -> None: + self.raw_url = url.replace("/rpc", "") + parsed_url = urlparse(url) + self.scheme = UrlScheme(parsed_url.scheme) + self.hostname = parsed_url.hostname + self.port = parsed_url.port diff --git a/src/surrealdb/connections/utils_mixin.py b/src/surrealdb/connections/utils_mixin.py new file mode 100644 index 00000000..2473ee71 --- /dev/null +++ b/src/surrealdb/connections/utils_mixin.py @@ -0,0 +1,13 @@ + + +class UtilsMixin: + + @staticmethod + def check_response_for_error(response: dict, process: str) -> None: + if response.get("error") is not None: + raise Exception(f"error {process}: {response.get('error')}") + + @staticmethod + def check_response_for_result(response: dict, process: str) -> None: + if "result" not in response.keys(): + raise Exception(f"no result {process}: {response}") diff --git a/surrealdb/data/README.md b/src/surrealdb/data/README.md similarity index 100% rename from surrealdb/data/README.md rename to src/surrealdb/data/README.md diff --git a/surrealdb/data/__init__.py b/src/surrealdb/data/__init__.py similarity index 100% rename from surrealdb/data/__init__.py rename to src/surrealdb/data/__init__.py diff --git a/surrealdb/data/cbor.py b/src/surrealdb/data/cbor.py similarity index 95% rename from surrealdb/data/cbor.py rename to src/surrealdb/data/cbor.py index aab2642f..ee0bce90 100644 --- a/surrealdb/data/cbor.py +++ b/src/surrealdb/data/cbor.py @@ -16,11 +16,11 @@ from surrealdb.data.types.range import BoundIncluded, BoundExcluded, Range from surrealdb.data.types.record_id import RecordID from surrealdb.data.types.table import Table -from surrealdb.errors import SurrealDbDecodeError, SurrealDbEncodeError @cbor2.shareable_encoder def default_encoder(encoder, obj): + if isinstance(obj, GeometryPoint): tagged = cbor2.CBORTag(constants.TAG_GEOMETRY_POINT, obj.get_coordinates()) @@ -73,7 +73,7 @@ def default_encoder(encoder, obj): ) else: - raise SurrealDbEncodeError("no encoder for type ", type(obj)) + raise BufferError("no encoder for type ", type(obj)) encoder.encode(tagged) @@ -125,7 +125,7 @@ def tag_decoder(decoder, tag, shareable_index=None): return DateTimeCompact.parse(tag.value[0], tag.value[1]) else: - raise SurrealDbDecodeError("no decoder for tag", tag.tag) + raise BufferError("no decoder for tag", tag.tag) def encode(obj): diff --git a/surrealdb/data/models.py b/src/surrealdb/data/models.py similarity index 93% rename from surrealdb/data/models.py rename to src/surrealdb/data/models.py index 83af1386..ea3bf861 100644 --- a/surrealdb/data/models.py +++ b/src/surrealdb/data/models.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field from typing import Any, Dict, List, Union -from surrealdb.errors import SurrealDbError from surrealdb.data.types.table import Table from surrealdb.data.types.record_id import RecordID @@ -56,7 +55,7 @@ def table_or_record_id(resource_str: str) -> Union[Table, RecordID]: if ":" in resource_str: table, record_id = resource_str.split(":") if len(table) == 0 or len(record_id) == 0: - raise SurrealDbError("invalid table or record id string") + raise BlockingIOError("invalid table or record id string") return RecordID(table, record_id) return Table(resource_str) diff --git a/tests/integration/blocking/__init__.py b/src/surrealdb/data/types/__init__.py similarity index 100% rename from tests/integration/blocking/__init__.py rename to src/surrealdb/data/types/__init__.py diff --git a/surrealdb/data/types/constants.py b/src/surrealdb/data/types/constants.py similarity index 100% rename from surrealdb/data/types/constants.py rename to src/surrealdb/data/types/constants.py diff --git a/src/surrealdb/data/types/datetime.py b/src/surrealdb/data/types/datetime.py new file mode 100644 index 00000000..19b4961a --- /dev/null +++ b/src/surrealdb/data/types/datetime.py @@ -0,0 +1,72 @@ +""" +Defines a compact representation of datetime using nanoseconds. +""" +from dataclasses import dataclass +from datetime import datetime +from typing import Tuple +import pytz # type: ignore +from math import floor + + +@dataclass +class DateTimeCompact: + """ + Represents a compact datetime object stored as a single integer value in nanoseconds. + + Attributes: + timestamp: The number of nanoseconds since the epoch (1970-01-01T00:00:00Z). + """ + timestamp: int = 0 # nanoseconds + + @staticmethod + def parse(seconds: int, nanoseconds: int) -> "DateTimeCompact": + """ + Creates a DateTimeCompact object from seconds and nanoseconds. + + Args: + seconds: The number of seconds since the epoch. + nanoseconds: The additional nanoseconds beyond the seconds. + + Returns: + A DateTimeCompact object representing the specified time. + """ + return DateTimeCompact(nanoseconds + (seconds * pow(10, 9))) + + def get_seconds_and_nano(self) -> Tuple[int, int]: + """ + Extracts the seconds and nanoseconds components from the timestamp. + + Returns: + A tuple containing: + - The number of seconds since the epoch. + - The remaining nanoseconds after the seconds. + """ + sec = floor(self.timestamp / pow(10, 9)) + nsec = self.timestamp - (sec * pow(10, 9)) + return sec, nsec + + def get_date_time(self, fmt: str = "%Y-%m-%dT%H:%M:%S.%fZ") -> str: + """ + Converts the timestamp into a formatted datetime string. + + Args: + fmt: The format string for the datetime. Defaults to ISO 8601 format. + + Returns: + A string representation of the datetime in the specified format. + """ + return datetime.fromtimestamp(self.timestamp / pow(10, 9), pytz.UTC).strftime(fmt) + + def __eq__(self, other: object) -> bool: + """ + Compares two DateTimeCompact objects for equality. + + Args: + other: The object to compare against. + + Returns: + True if the objects have the same timestamp, False otherwise. + """ + if isinstance(other, DateTimeCompact): + return self.timestamp == other.timestamp + return False diff --git a/src/surrealdb/data/types/duration.py b/src/surrealdb/data/types/duration.py new file mode 100644 index 00000000..0ec87001 --- /dev/null +++ b/src/surrealdb/data/types/duration.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass +from typing import Tuple, Union +from math import floor, pow + +UNITS = { + "ns": 1, + "us": int(1e3), + "ms": int(1e6), + "s": int(1e9), + "m": int(60 * 1e9), + "h": int(3600 * 1e9), + "d": int(86400 * 1e9), + "w": int(604800 * 1e9), +} + +@dataclass +class Duration: + elapsed: int = 0 # nanoseconds + + @staticmethod + def parse(value: Union[str, int], nanoseconds: int = 0) -> "Duration": + if isinstance(value, int): + return Duration(nanoseconds + value * UNITS["s"]) + elif isinstance(value, str): + unit = value[-1] + num = int(value[:-1]) + if unit in UNITS: + return Duration(num * UNITS[unit]) + else: + raise ValueError(f"Unknown duration unit: {unit}") + else: + raise TypeError("Duration must be initialized with an int or str") + + def get_seconds_and_nano(self) -> Tuple[int, int]: + sec = floor(self.elapsed / UNITS["s"]) + nsec = self.elapsed - (sec * UNITS["s"]) + return sec, nsec + + def __eq__(self, other: object) -> bool: + if isinstance(other, Duration): + return self.elapsed == other.elapsed + return False + + @property + def nanoseconds(self) -> int: + return self.elapsed + + @property + def microseconds(self) -> int: + return self.elapsed // UNITS["us"] + + @property + def milliseconds(self) -> int: + return self.elapsed // UNITS["ms"] + + @property + def seconds(self) -> int: + return self.elapsed // UNITS["s"] + + @property + def minutes(self) -> int: + return self.elapsed // UNITS["m"] + + @property + def hours(self) -> int: + return self.elapsed // UNITS["h"] + + @property + def days(self) -> int: + return self.elapsed // UNITS["d"] + + @property + def weeks(self) -> int: + return self.elapsed // UNITS["w"] + + def to_string(self) -> str: + for unit in reversed(["w", "d", "h", "m", "s", "ms", "us", "ns"]): + value = self.elapsed // UNITS[unit] + if value > 0: + return f"{value}{unit}" + return "0ns" + + def to_compact(self) -> list: + return [self.elapsed // UNITS["s"]] diff --git a/src/surrealdb/data/types/future.py b/src/surrealdb/data/types/future.py new file mode 100644 index 00000000..eca6e4cf --- /dev/null +++ b/src/surrealdb/data/types/future.py @@ -0,0 +1,31 @@ +""" +Defines a simple Future class to hold a value of any type. +""" + +from dataclasses import dataclass +from typing import Any + + +@dataclass +class Future: + """ + Represents a placeholder for a value that may be unset or resolved in the future. + + Attributes: + value: The value held by the Future object. This can be of any type. + """ + value: Any + + def __eq__(self, other: object) -> bool: + """ + Compares two Future objects for equality. + + Args: + other: The object to compare against. + + Returns: + True if the values held by both Future objects are equal, False otherwise. + """ + if isinstance(other, Future): + return self.value == other.value + return False diff --git a/src/surrealdb/data/types/geometry.py b/src/surrealdb/data/types/geometry.py new file mode 100644 index 00000000..dc449cda --- /dev/null +++ b/src/surrealdb/data/types/geometry.py @@ -0,0 +1,314 @@ +""" +Defines a unset of geometry classes for representing geometric shapes such as points, lines, polygons, and collections. +""" + +from dataclasses import dataclass +from typing import List, Tuple + + +class Geometry: + """ + Base class for all geometry types. Provides the interface for retrieving coordinates + and parsing them into specific geometry types. + """ + + def get_coordinates(self): + """ + Returns the coordinates of the geometry. Should be implemented by subclasses. + """ + pass + + @staticmethod + def parse_coordinates(coordinates): + """ + Parses a list of coordinates into a specific geometry type. Should be implemented by subclasses. + """ + pass + + +@dataclass +class GeometryPoint(Geometry): + """ + Represents a single point in a 2D space. + + Attributes: + longitude: The longitude of the point. + latitude: The latitude of the point. + """ + longitude: float + latitude: float + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(longitude={self.longitude}, latitude={self.latitude})" + + def get_coordinates(self) -> Tuple[float, float]: + """ + Returns the coordinates of the point. + + Returns: + A tuple of (longitude, latitude). + """ + return self.longitude, self.latitude + + @staticmethod + def parse_coordinates(coordinates: Tuple[float, float]) -> "GeometryPoint": + """ + Parses a tuple of coordinates into a GeometryPoint. + + Args: + coordinates: A tuple containing longitude and latitude. + + Returns: + A GeometryPoint object. + """ + return GeometryPoint(coordinates[0], coordinates[1]) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryPoint): + return self.longitude == other.longitude and self.latitude == other.latitude + return False + + +@dataclass +class GeometryLine(Geometry): + """ + Represents a line defined by two or more points. + + Attributes: + geometry_points: A list of GeometryPoint objects defining the line. + """ + geometry_points: List[GeometryPoint] + + def __init__(self, point1: GeometryPoint, point2: GeometryPoint, *other_points: GeometryPoint) -> None: + """ + The constructor for the GeometryLine class. + """ + self.geometry_points = [point1, point2] + list(other_points) + + def get_coordinates(self) -> List[Tuple[float, float]]: + """ + Returns the coordinates of the line as a list of tuples. + + Returns: + A list of (longitude, latitude) tuples. + """ + return [point.get_coordinates() for point in self.geometry_points] + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_points)})' + + @staticmethod + def parse_coordinates(coordinates: List[Tuple[float, float]]) -> "GeometryLine": + """ + Parses a list of coordinate tuples into a GeometryLine. + + Args: + coordinates: A list of tuples containing longitude and latitude. + + Returns: + A GeometryLine object. + """ + return GeometryLine(*[GeometryPoint.parse_coordinates(point) for point in coordinates]) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryLine): + return self.geometry_points == other.geometry_points + return False + + +@dataclass +class GeometryPolygon(Geometry): + """ + Represents a polygon defined by multiple lines. + + Attributes: + geometry_lines: A list of GeometryLine objects defining the polygon. + """ + geometry_lines: List[GeometryLine] + + def __init__(self, line1: GeometryLine, line2: GeometryLine, *other_lines: GeometryLine): + self.geometry_lines = [line1, line2] + list(other_lines) + + def get_coordinates(self) -> List[List[Tuple[float, float]]]: + """ + Returns the coordinates of the polygon as a list of lines, each containing a list of coordinate tuples. + + Returns: + A list of lists of (longitude, latitude) tuples. + """ + return [line.get_coordinates() for line in self.geometry_lines] + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_lines)})' + + @staticmethod + def parse_coordinates(coordinates: List[List[Tuple[float, float]]]) -> "GeometryPolygon": + """ + Parses a list of lines, each defined by a list of coordinate tuples, into a GeometryPolygon. + + Args: + coordinates: A list of lists containing longitude and latitude tuples. + + Returns: + A GeometryPolygon object. + """ + return GeometryPolygon(*[GeometryLine.parse_coordinates(line) for line in coordinates]) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryPolygon): + return self.geometry_lines == other.geometry_lines + return False + + +@dataclass +class GeometryMultiPoint(Geometry): + """ + Represents multiple points in 2D space. + + Attributes: + geometry_points: A list of GeometryPoint objects. + """ + geometry_points: List[GeometryPoint] + + def __init__(self, *geometry_points: GeometryPoint): + self.geometry_points = list(geometry_points) + + def get_coordinates(self) -> List[Tuple[float, float]]: + """ + Returns the coordinates of all points as a list of tuples. + + Returns: + A list of (longitude, latitude) tuples. + """ + return [point.get_coordinates() for point in self.geometry_points] + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_points)})' + + @staticmethod + def parse_coordinates(coordinates: List[Tuple[float, float]]) -> "GeometryMultiPoint": + """ + Parses a list of coordinate tuples into a GeometryMultiPoint. + + Args: + coordinates: A list of tuples containing longitude and latitude. + + Returns: + A GeometryMultiPoint object. + """ + return GeometryMultiPoint(*[GeometryPoint.parse_coordinates(point) for point in coordinates]) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryMultiPoint): + return self.geometry_points == other.geometry_points + return False + + +@dataclass +class GeometryMultiLine(Geometry): + """ + Represents multiple lines. + + Attributes: + geometry_lines: A list of GeometryLine objects. + """ + geometry_lines: List[GeometryLine] + + def __init__(self, *geometry_lines: GeometryLine): + self.geometry_lines = list(geometry_lines) + + def get_coordinates(self) -> List[List[Tuple[float, float]]]: + """ + Returns the coordinates of all lines as a list of lists of tuples. + + Returns: + A list of lists of (longitude, latitude) tuples. + """ + return [line.get_coordinates() for line in self.geometry_lines] + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_lines)})' + + @staticmethod + def parse_coordinates(coordinates: List[List[Tuple[float, float]]]) -> "GeometryMultiLine": + """ + Parses a list of lines, each defined by a list of coordinate tuples, into a GeometryMultiLine. + + Args: + coordinates: A list of lists containing longitude and latitude tuples. + + Returns: + A GeometryMultiLine object. + """ + return GeometryMultiLine(*[GeometryLine.parse_coordinates(line) for line in coordinates]) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryMultiLine): + return self.geometry_lines == other.geometry_lines + return False + + +@dataclass +class GeometryMultiPolygon(Geometry): + """ + Represents multiple polygons. + + Attributes: + geometry_polygons: A list of GeometryPolygon objects. + """ + geometry_polygons: List[GeometryPolygon] + + def __init__(self, *geometry_polygons: GeometryPolygon): + self.geometry_polygons = list(geometry_polygons) + + def get_coordinates(self) -> List[List[List[Tuple[float, float]]]]: + """ + Returns the coordinates of all polygons as a list of lists of lines, each containing a list of tuples. + + Returns: + A list of lists of lists of (longitude, latitude) tuples. + """ + return [polygon.get_coordinates() for polygon in self.geometry_polygons] + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_polygons)})' + + @staticmethod + def parse_coordinates(coordinates: List[List[List[Tuple[float, float]]]]) -> "GeometryMultiPolygon": + """ + Parses a list of polygons, each defined by a list of lines, into a GeometryMultiPolygon. + + Args: + coordinates: A list of lists of lists containing longitude and latitude tuples. + + Returns: + A GeometryMultiPolygon object. + """ + return GeometryMultiPolygon(*[GeometryPolygon.parse_coordinates(polygon) for polygon in coordinates]) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryMultiPolygon): + return self.geometry_polygons == other.geometry_polygons + return False + + +@dataclass +class GeometryCollection: + """ + Represents a collection of multiple geometry objects. + + Attributes: + geometries: A list of Geometry objects. + """ + geometries: List[Geometry] + + def __init__(self, *geometries: Geometry): + self.geometries = list(geometries) + + def __repr__(self) -> str: + return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometries)})' + + def __eq__(self, other: object) -> bool: + if isinstance(other, GeometryCollection): + return self.geometries == other.geometries + return False diff --git a/src/surrealdb/data/types/range.py b/src/surrealdb/data/types/range.py new file mode 100644 index 00000000..20fdd7b8 --- /dev/null +++ b/src/surrealdb/data/types/range.py @@ -0,0 +1,130 @@ +""" +Defines classes for representing bounded ranges, including inclusive and exclusive bounds. +""" + +from dataclasses import dataclass + + +class Bound: + """ + Represents a generic boundary for a range. This is an abstract base class + that can be extended by specific bound types, such as inclusive or exclusive bounds. + """ + + def __init__(self): + """ + Initializes a generic bound. + """ + pass + + def __eq__(self, other: object) -> bool: + """ + Compares two Bound objects for equality. Must be overridden by subclasses. + + Args: + other: The object to compare against. + + Returns: + True if the objects are equal, False otherwise. + """ + return isinstance(other, Bound) + + +@dataclass +class BoundIncluded(Bound): + """ + Represents an inclusive bound of a range. + + Attributes: + value: The value of the inclusive bound. + """ + + value: any + + def __init__(self, value): + """ + Initializes an inclusive bound with a specific value. + + Args: + value: The value of the bound. + """ + super().__init__() + self.value = value + + def __eq__(self, other: object) -> bool: + """ + Compares two BoundIncluded objects for equality. + + Args: + other: The object to compare against. + + Returns: + True if the objects have the same value, False otherwise. + """ + if isinstance(other, BoundIncluded): + return self.value == other.value + return False + + +@dataclass +class BoundExcluded(Bound): + """ + Represents an exclusive bound of a range. + + Attributes: + value: The value of the exclusive bound. + """ + + value: any + + def __init__(self, value): + """ + Initializes an exclusive bound with a specific value. + + Args: + value: The value of the bound. + """ + super().__init__() + self.value = value + + def __eq__(self, other: object) -> bool: + """ + Compares two BoundExcluded objects for equality. + + Args: + other: The object to compare against. + + Returns: + True if the objects have the same value, False otherwise. + """ + if isinstance(other, BoundExcluded): + return self.value == other.value + return False + + +@dataclass +class Range: + """ + Represents a range with a beginning and an end bound. + + Attributes: + begin: The starting bound of the range (inclusive or exclusive). + end: The ending bound of the range (inclusive or exclusive). + """ + + begin: Bound + end: Bound + + def __eq__(self, other: object) -> bool: + """ + Compares two Range objects for equality. + + Args: + other: The object to compare against. + + Returns: + True if the beginning and ending bounds are equal, False otherwise. + """ + if isinstance(other, Range): + return self.begin == other.begin and self.end == other.end + return False diff --git a/src/surrealdb/data/types/record_id.py b/src/surrealdb/data/types/record_id.py new file mode 100644 index 00000000..152800e1 --- /dev/null +++ b/src/surrealdb/data/types/record_id.py @@ -0,0 +1,58 @@ +""" +Defines the data type for the record ID. +""" +from dataclasses import dataclass + + +class RecordID: + """ + An identifier of the record. This class houses the ID of the row, and the table name. + + Attributes: + table_name: The table name associated with the record ID + identifier: The ID of the row + """ + def __init__(self, table_name: str, identifier) -> None: + """ + The constructor for the RecordID class. + + Args: + table_name: The table name associated with the record ID + identifier: The ID of the row + """ + self.table_name = table_name + self.id = identifier + + def __str__(self) -> str: + return f"{self.table_name}:{self.id}" + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(table_name={self.table_name}, record_id={self.id})".format( + self=self + ) + + def __eq__(self, other): + if isinstance(other, RecordID): + return ( + self.table_name == other.table_name and + self.id == other.id + ) + + @staticmethod + def parse(record_str: str) -> "RecordID": + """ + Converts a string to a RecordID object. + + Args: + record_str: The string representation of the record ID + + Returns: A RecordID object. + + """ + if ":" not in record_str: + raise ValueError( + 'invalid string provided for parse. the expected string format is "table_name:record_id"' + ) + + table, record_id = record_str.split(":") + return RecordID(table, record_id) diff --git a/src/surrealdb/data/types/table.py b/src/surrealdb/data/types/table.py new file mode 100644 index 00000000..2436ddce --- /dev/null +++ b/src/surrealdb/data/types/table.py @@ -0,0 +1,52 @@ +""" +Defines a Table class to represent a database table by its name. +""" + +class Table: + """ + Represents a database table by its name. + + Attributes: + table_name: The name of the table. + """ + + def __init__(self, table_name: str) -> None: + """ + Initializes a Table object with a specific table name. + + Args: + table_name: The name of the table. + """ + self.table_name = table_name + + def __str__(self) -> str: + """ + Returns a string representation of the table. + + Returns: + The name of the table as a string. + """ + return f"{self.table_name}" + + def __repr__(self) -> str: + """ + Returns a string representation of the table for debugging purposes. + + Returns: + The name of the table as a string. + """ + return f"{self.table_name}" + + def __eq__(self, other: object) -> bool: + """ + Compares two Table objects for equality. + + Args: + other: The object to compare against. + + Returns: + True if the table names are equal, False otherwise. + """ + if isinstance(other, Table): + return self.table_name == other.table_name + return False diff --git a/src/surrealdb/data/utils.py b/src/surrealdb/data/utils.py new file mode 100644 index 00000000..cdfe2222 --- /dev/null +++ b/src/surrealdb/data/utils.py @@ -0,0 +1,19 @@ +""" +Utils for handling processes around data +""" +from typing import Union + +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +def process_thing(thing: Union[str, RecordID, Table]) -> Union[RecordID, Table]: + if isinstance(thing, RecordID): + return thing + elif isinstance(thing, Table): + return thing + elif isinstance(thing, str): + if ":" in thing: + return RecordID.parse(thing) + else: + return Table(thing) diff --git a/src/surrealdb/errors.py b/src/surrealdb/errors.py new file mode 100644 index 00000000..fccecc56 --- /dev/null +++ b/src/surrealdb/errors.py @@ -0,0 +1,8 @@ + + +class SurrealDBMethodError(Exception): + def __init__(self, message): + self.message = message + + def __str__(self): + return self.message diff --git a/tests/scripts/__init__.py b/src/surrealdb/request_message/__init__.py similarity index 100% rename from tests/scripts/__init__.py rename to src/surrealdb/request_message/__init__.py diff --git a/tests/unit/__init__.py b/src/surrealdb/request_message/descriptors/__init__.py similarity index 100% rename from tests/unit/__init__.py rename to src/surrealdb/request_message/descriptors/__init__.py diff --git a/src/surrealdb/request_message/descriptors/cbor_ws.py b/src/surrealdb/request_message/descriptors/cbor_ws.py new file mode 100644 index 00000000..f443484c --- /dev/null +++ b/src/surrealdb/request_message/descriptors/cbor_ws.py @@ -0,0 +1,574 @@ +from cerberus import Validator +from cerberus.errors import ValidationError + +from surrealdb.data.cbor import encode +from surrealdb.request_message.methods import RequestMethod +from surrealdb.data.utils import process_thing +from surrealdb.data.types.table import Table + + +class WsCborDescriptor: + def __get__(self, obj, type=None) -> bytes: + if obj.method == RequestMethod.USE: + return self.prep_use(obj) + elif obj.method == RequestMethod.INFO: + return self.prep_info(obj) + elif obj.method == RequestMethod.VERSION: + return self.prep_version(obj) + elif obj.method == RequestMethod.SIGN_UP: + return self.prep_signup(obj) + elif obj.method == RequestMethod.SIGN_IN: + return self.prep_signin(obj) + elif obj.method == RequestMethod.AUTHENTICATE: + return self.prep_authenticate(obj) + elif obj.method == RequestMethod.INVALIDATE: + return self.prep_invalidate(obj) + elif obj.method == RequestMethod.LET: + return self.prep_let(obj) + elif obj.method == RequestMethod.UNSET: + return self.prep_unset(obj) + elif obj.method == RequestMethod.LIVE: + return self.prep_live(obj) + elif obj.method == RequestMethod.KILL: + return self.prep_kill(obj) + elif obj.method == RequestMethod.QUERY: + return self.prep_query(obj) + elif obj.method == RequestMethod.INSERT: + return self.prep_insert(obj) + elif obj.method == RequestMethod.PATCH: + return self.prep_patch(obj) + elif obj.method == RequestMethod.SELECT: + return self.prep_select(obj) + elif obj.method == RequestMethod.CREATE: + return self.prep_create(obj) + elif obj.method == RequestMethod.UPDATE: + return self.prep_update(obj) + elif obj.method == RequestMethod.MERGE: + return self.prep_merge(obj) + elif obj.method == RequestMethod.DELETE: + return self.prep_delete(obj) + elif obj.method == RequestMethod.INSERT_RELATION: + return self.prep_insert_relation(obj) + elif obj.method == RequestMethod.UPSERT: + return self.prep_upsert(obj) + + raise ValueError(f"Invalid method for Cbor WS encoding: {obj.method}") + + def _raise_invalid_schema(self, data:dict, schema: dict, method: str) -> None: + v = Validator(schema) + if not v.validate(data): + raise ValueError(f"Invalid schema for Cbor WS encoding for {method}: {v.errors}") + + def prep_use(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [obj.kwargs.get("namespace"), obj.kwargs.get("database")], + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True}, # "method" must be a string + "params": { + "type": "list", # "params" must be a list + "schema": {"type": "string"}, # Elements of "params" must be strings + "required": True + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_info(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True}, # "method" must be a string + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_version(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True}, # "method" must be a string + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_signup(self, obj) -> bytes: + passed_params = obj.kwargs.get("data") + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "NS": passed_params.get("namespace"), + "DB": passed_params.get("database"), + "AC": passed_params.get("access"), + } + ], + } + for key, value in passed_params["variables"].items(): + data["params"][0][key] = value + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True}, # "method" must be a string + "params": { + "type": "list", # "params" must be a list + "schema": { + "type": "dict", # Each element of the "params" list must be a dictionary + "schema": { + "NS": {"type": "string", "required": True}, # "NS" must be a string + "DB": {"type": "string", "required": True}, # "DB" must be a string + "AC": {"type": "string", "required": True}, # "AC" must be a string + "username": {"type": "string", "required": True}, # "username" must be a string + "password": {"type": "string", "required": True}, # "password" must be a string + }, + }, + "required": True, + }, + } + # self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_signin(self, obj) -> bytes: + """ + - user+pass -> done + - user+pass+ac -> done + - user+pass+ns -> done + - user+pass+ns+ac -> done + - user+pass+ns+db + - user+pass+ns+db+ac + - ns+db+ac+any other vars + """ + if obj.kwargs.get("namespace") is None: + # root user signing in + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + ] + } + elif obj.kwargs.get("namespace") is None and obj.kwargs.get("access") is not None: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "ac": obj.kwargs.get("access"), + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + ] + } + elif obj.kwargs.get("database") is None and obj.kwargs.get("access") is None: + # namespace signin + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "ns": obj.kwargs.get("namespace"), + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + ] + } + elif obj.kwargs.get("database") is None and obj.kwargs.get("access") is not None: + # access signin + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "ns": obj.kwargs.get("namespace"), + "ac": obj.kwargs.get("access"), + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + ] + } + elif obj.kwargs.get("database") is not None and obj.kwargs.get("namespace") is not None and obj.kwargs.get("access") is not None and obj.kwargs.get("variables") is None: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "ns": obj.kwargs.get("namespace"), + "db": obj.kwargs.get("database"), + "ac": obj.kwargs.get("access"), + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + ] + } + + elif obj.kwargs.get("username") is None and obj.kwargs.get("password") is None: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "ns": obj.kwargs.get("namespace"), + "db": obj.kwargs.get("database"), + "ac": obj.kwargs.get("access"), + # "variables": obj.kwargs.get("variables") + } + ] + } + for key, value in obj.kwargs.get("variables", {}).items(): + data["params"][0][key] = value + + elif obj.kwargs.get("database") is not None and obj.kwargs.get("namespace") is not None and obj.kwargs.get("access") is None: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + { + "ns": obj.kwargs.get("namespace"), + "db": obj.kwargs.get("database"), + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + ] + } + + else: + raise ValueError(f"Invalid data for signin: {obj.kwargs}") + return encode(data) + + def prep_authenticate(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + obj.kwargs.get("token") + ] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["authenticate"]}, + "params": { + "type": "list", + "schema": { + "type": "string", + "regex": r"^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$", # Matches JWT format + }, + "required": True, + "minlength": 1, + "maxlength": 1, # Ensures exactly one token in the list + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_invalidate(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True} + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_let(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [obj.kwargs.get("key"), obj.kwargs.get("value")] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["let"]}, + "params": { + "type": "list", + "minlength": 2, + "required": True + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_unset(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": obj.kwargs.get("params") + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["unset"]}, + "params": { + "type": "list", + "required": True + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_live(self, obj) -> bytes: + table = obj.kwargs.get("table") + if isinstance(table, str): + table = Table(table) + data = { + "id": obj.id, + "method": obj.method.value, + "params": [table] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["live"]}, + "params": { + "type": "list", + "required": True + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_kill(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [obj.kwargs.get("uuid")] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["kill"]}, + "params": { + "type": "list", + "required": True + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_query(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + obj.kwargs.get("query"), + obj.kwargs.get("params", dict()) + ] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["query"]}, + "params": { + "type": "list", + "minlength": 2, # Ensures there are at least two elements + "maxlength": 2, # Ensures exactly two elements + "required": True + }, + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_insert(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + process_thing(obj.kwargs.get("collection")), + obj.kwargs.get("params") + ] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["insert"]}, + "params": { + "type": "list", + "minlength": 2, # Ensure there are at least two elements + "maxlength": 2, # Ensure exactly two elements + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_patch(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + process_thing(obj.kwargs.get("collection")), + obj.kwargs.get("params") + ] + } + if obj.kwargs.get("params") is None: + raise ValidationError("parameters cannot be None for a patch method") + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["patch"]}, + "params": { + "type": "list", + "minlength": 2, # Ensure there are at least two elements + "maxlength": 2, # Ensure exactly two elements + "required": True, + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_select(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": obj.kwargs.get("params") + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["select"]}, + "params": { + "type": "list", + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_create(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [process_thing(obj.kwargs.get("collection"))] + } + if obj.kwargs.get("data"): + data["params"].append(obj.kwargs.get("data")) + + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["create"]}, + "params": { + "type": "list", + "minlength": 1, + "maxlength": 2, + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_update(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + process_thing(obj.kwargs.get("record_id")), + obj.kwargs.get("data", dict()) + ] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["update"]}, + "params": { + "type": "list", + "minlength": 1, + "maxlength": 2, + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_merge(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + process_thing(obj.kwargs.get("record_id")), + obj.kwargs.get("data", dict()) + ] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["merge"]}, + "params": { + "type": "list", + "minlength": 1, + "maxlength": 2, + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_delete(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [process_thing(obj.kwargs.get("record_id"))] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["delete"]}, + "params": { + "type": "list", + "minlength": 1, + "maxlength": 1, + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_insert_relation(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + Table(obj.kwargs.get("table")), + ] + } + params = obj.kwargs.get("params", []) + # for i in params: + data["params"].append(params) + + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["insert_relation"]}, + "params": { + "type": "list", + "minlength": 2, + "maxlength": 2, + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) + + def prep_upsert(self, obj) -> bytes: + data = { + "id": obj.id, + "method": obj.method.value, + "params": [ + process_thing(obj.kwargs.get("record_id")), + obj.kwargs.get("data", dict()) + ] + } + schema = { + "id": {"required": True}, + "method": {"type": "string", "required": True, "allowed": ["upsert"]}, + "params": { + "type": "list", + "minlength": 1, + "maxlength": 2, + "required": True + } + } + self._raise_invalid_schema(data=data, schema=schema, method=obj.method.value) + return encode(data) diff --git a/src/surrealdb/request_message/descriptors/json_http.py b/src/surrealdb/request_message/descriptors/json_http.py new file mode 100644 index 00000000..1d781900 --- /dev/null +++ b/src/surrealdb/request_message/descriptors/json_http.py @@ -0,0 +1,85 @@ +from marshmallow import ValidationError +from surrealdb.request_message.methods import RequestMethod +from marshmallow import Schema, fields +from typing import Tuple +from enum import Enum +import json + + +class HttpMethod(Enum): + GET = "GET" + POST = "POST" + PUT = "PUT" + PATCH = "PATCH" + DELETE = "DELETE" + + +class JsonHttpDescriptor: + def __get__(self, obj, type=None) -> Tuple[str, HttpMethod, str]: + if obj.method == RequestMethod.SIGN_IN: + return self.prep_signin(obj) + # if obj.method == RequestMethod.USE: + # return self.prep_use(obj) + # elif obj.method == RequestMethod.INFO: + # return self.prep_info(obj) + # elif obj.method == RequestMethod.VERSION: + # return self.prep_version(obj) + # elif obj.method == RequestMethod.SIGN_UP: + # return self.prep_signup(obj) + # elif obj.method == RequestMethod.SIGN_IN: + # return self.prep_signin(obj) + # elif obj.method == RequestMethod.AUTHENTICATE: + # return self.prep_authenticate(obj) + # elif obj.method == RequestMethod.INVALIDATE: + # return self.prep_invalidate(obj) + # elif obj.method == RequestMethod.LET: + # return self.prep_let(obj) + # elif obj.method == RequestMethod.UNSET: + # return self.prep_unset(obj) + # elif obj.method == RequestMethod.LIVE: + # return self.prep_live(obj) + # elif obj.method == RequestMethod.KILL: + # return self.prep_kill(obj) + # elif obj.method == RequestMethod.QUERY: + # return self.prep_query(obj) + # elif obj.method == RequestMethod.INSERT: + # return self.prep_insert(obj) + # elif obj.method == RequestMethod.PATCH: + # return self.prep_patch(obj) + + @staticmethod + def serialize(data: dict, schema: Schema, context: str) -> str: + try: + result = schema.load(data) + except ValidationError as err: + raise ValidationError(f"Validation error for {context}:", err.messages) + return json.dumps(schema.dump(result)) + + + def prep_signin(self, obj) -> Tuple[str, HttpMethod, str]: + class SignInSchema(Schema): + ns = fields.Str(required=False) # Optional Namespace + db = fields.Str(required=False) # Optional Database + ac = fields.Str(required=False) # Optional Account category + user = fields.Str(required=True) # Required Username + pass_ = fields.Str(required=True, data_key="pass") # Required Password + + schema = SignInSchema() + + if obj.kwargs.get("namespace") is None: + # root user signing in + data = { + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + else: + data = { + "ns": obj.kwargs.get("namespace"), + "db": obj.kwargs.get("database"), + "ac": obj.kwargs.get("account"), + "user": obj.kwargs.get("username"), + "pass": obj.kwargs.get("password") + } + + result = self.serialize(data, schema, "HTTP signin") + return result, HttpMethod.POST, "signin" diff --git a/src/surrealdb/request_message/message.py b/src/surrealdb/request_message/message.py new file mode 100644 index 00000000..dfc8774b --- /dev/null +++ b/src/surrealdb/request_message/message.py @@ -0,0 +1,14 @@ +from surrealdb.request_message.descriptors.cbor_ws import WsCborDescriptor +from surrealdb.request_message.descriptors.json_http import JsonHttpDescriptor +from surrealdb.request_message.methods import RequestMethod + + +class RequestMessage: + + WS_CBOR_DESCRIPTOR = WsCborDescriptor() + JSON_HTTP_DESCRIPTOR = JsonHttpDescriptor() + + def __init__(self, id_for_request, method: RequestMethod, **kwargs) -> None: + self.id = id_for_request + self.method = method + self.kwargs = kwargs diff --git a/src/surrealdb/request_message/methods.py b/src/surrealdb/request_message/methods.py new file mode 100644 index 00000000..e83d591d --- /dev/null +++ b/src/surrealdb/request_message/methods.py @@ -0,0 +1,30 @@ +from enum import Enum + + +class RequestMethod(Enum): + USE = "use" + SIGN_IN = "signin" + SIGN_UP = "signup" + INFO = "info" + VERSION = "version" + AUTHENTICATE = "authenticate" + INVALIDATE = "invalidate" + LET = "let" + UNSET = "unset" + SELECT = "select" + QUERY = "query" + CREATE = "create" + INSERT = "insert" + INSERT_RELATION = "insert_relation" + PATCH = "patch" + MERGE = "merge" + UPDATE = "update" + UPSERT = "upsert" + DELETE = "delete" + LIVE = "live" + KILL = "kill" + POST = "post" + + @staticmethod + def from_string(method: str) -> "RequestMethod": + return RequestMethod(method.lower()) diff --git a/src/surrealdb/request_message/sql_adapter.py b/src/surrealdb/request_message/sql_adapter.py new file mode 100644 index 00000000..d09ba86a --- /dev/null +++ b/src/surrealdb/request_message/sql_adapter.py @@ -0,0 +1,75 @@ +""" +Defines a class that adapts SQL commands from various sources into a single string. +""" +from typing import List + + +class SqlAdapter: + """ + Adapts SQL commands from various sources into a single string. + """ + @staticmethod + def from_list(commands: List[str]) -> str: + """ + Converts a list of SQL commands into a single string. + + :param commands: (List[str]) the list of commands to create the migration from + :return: (str) a series of SQL commands as a single string + """ + buffer = [] + for i in commands: + if i == "": + pass + else: + if i[-1] != ";": + i += ";" + buffer.append(i) + return " ".join(buffer) + + @staticmethod + def from_docstring(docstring: str) -> str: + """ + Creates a single SQL string from a docstring. + + :param docstring: (str) the docstring to create the migration from + :return: (str) a series of SQL commands as a single string + """ + buffer = docstring.replace("\n", "").replace(" ", "").split(";") + final_buffer = [] + for i in buffer: + if i == "": + pass + else: + final_buffer.append(i + ";") + return " ".join(final_buffer) + + @staticmethod + def from_file(file_path: str) -> str: + """ + Creates a single SQL string from a file. + + :param file_path: (str) the path to the file to create the migration from + :return: (str) a series of SQL commands as a single string + """ + buffer = [] + with open(file_path, "r") as file: + raw_buffer = file.read().split("\n") + for i in raw_buffer: + if i == "": + pass + elif i[0:2] == "--": + pass + else: + buffer.append(i) + cleaned_string = " ".join(buffer) + cleaned_buffer = cleaned_string.split(";") + final_buffer = [] + for i in cleaned_buffer: + if i == "": + pass + else: + if i[0] == " ": + final_buffer.append(i[1:] + ";") + else: + final_buffer.append(i + ";") + return " ".join(final_buffer) diff --git a/surrealdb/VERSION.txt b/surrealdb/VERSION.txt deleted file mode 100644 index b217dcd1..00000000 --- a/surrealdb/VERSION.txt +++ /dev/null @@ -1 +0,0 @@ -VERSION='0.1.0' \ No newline at end of file diff --git a/surrealdb/__init__.py b/surrealdb/__init__.py deleted file mode 100644 index c43faea8..00000000 --- a/surrealdb/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from surrealdb.async_surrealdb import AsyncSurrealDB -from surrealdb.surrealdb import SurrealDB -from surrealdb.errors import ( - SurrealDbError, - SurrealDbConnectionError, - SurrealDbDecodeError, - SurrealDbEncodeError, -) - -from surrealdb.data.models import Patch, QueryResponse, GraphQLOptions -from surrealdb.data.types.duration import Duration -from surrealdb.data.types.future import Future -from surrealdb.data.types.geometry import ( - Geometry, - GeometryPoint, - GeometryLine, - GeometryPolygon, - GeometryMultiPoint, - GeometryMultiLine, - GeometryMultiPolygon, - GeometryCollection, -) -from surrealdb.data.types.range import Bound, BoundIncluded, BoundExcluded, Range -from surrealdb.data.types.record_id import RecordID -from surrealdb.data.types.table import Table - -__all__ = ( - "SurrealDB", - "AsyncSurrealDB", - "SurrealDbError", - "SurrealDbConnectionError", - "SurrealDbDecodeError", - "SurrealDbEncodeError", - "Patch", - "QueryResponse", - "GraphQLOptions", - "Duration", - "Future", - "Geometry", - "GeometryPoint", - "GeometryLine", - "GeometryPolygon", - "GeometryMultiPoint", - "GeometryMultiLine", - "GeometryMultiPolygon", - "GeometryCollection", - "Bound", - "BoundIncluded", - "BoundExcluded", - "Range", - "RecordID", - "Table", -) diff --git a/surrealdb/async_surrealdb.py b/surrealdb/async_surrealdb.py deleted file mode 100644 index 386dc4d5..00000000 --- a/surrealdb/async_surrealdb.py +++ /dev/null @@ -1,299 +0,0 @@ -""" -This file defines the interface for the AsyncSurrealDB. - -# Usage -The database can be used by the following code: -```python -from surrealdb.async_surrealdb import AsyncSurrealDB - -db = SurrealDB(url="ws://localhost:8080") -await db.connect() -await db.use("ns", "db_name") -``` -It can also be used as a context manager: -```python -from surrealdb.async_surrealdb import AsyncSurrealDB - -async with SurrealDB("ws://localhost:8080") as db: - await db.use("ns", "db_name") -``` -""" - -import asyncio -import logging -import uuid -from typing import Union, List -from typing_extensions import Self - -from surrealdb.constants import ( - METHOD_KILL, - METHOD_LIVE, - METHOD_MERGE, - METHOD_DELETE, - METHOD_UPSERT, - METHOD_UPDATE, - METHOD_PATCH, - METHOD_INSERT, - METHOD_CREATE, - METHOD_QUERY, - METHOD_SELECT, - METHOD_VERSION, - METHOD_INFO, - METHOD_INVALIDATE, - METHOD_AUTHENTICATE, - METHOD_SIGN_UP, - METHOD_SIGN_IN, -) -from surrealdb.connection_factory import create_connection_factory -from surrealdb.data import Table, RecordID, Patch, QueryResponse - - -class AsyncSurrealDB: - """This class is responsible for managing the connection to SurrealDB and managing operations on the connection.""" - - def __init__(self, url: str | None = None, timeout: int = 0) -> None: - """ - The constructor for the SurrealDB class. - - :param url: the url to connect to SurrealDB with - """ - logger = logging.getLogger(__name__) - self.__connection = create_connection_factory( - connection_url=url, timeout=timeout, logger=logger - ) - - async def __aenter__(self): - await self.connect() - return self - - async def __aexit__(self, *args): - await self.close() - - async def connect(self) -> Self: - """Connect to SurrealDB.""" - await self.__connection.connect() - return self - - async def close(self): - """Close connection to SurrealDB.""" - await self.__connection.close() - - async def use(self, namespace: str, database: str) -> None: - """ - Uses the given namespace and database in the connection. - - :param namespace: the namespace to use - :param database: the database to use - :return: None - """ - await self.__connection.use(namespace=namespace, database=database) - - async def sign_in(self, username: str, password: str) -> str: - """ - Signs in to the database. - - :param password: the password to sign in with - :param username: the username to sign in with - - :return: str - """ - token = await self.__connection.send( - METHOD_SIGN_IN, {"user": username, "pass": password} - ) - self.__connection.set_token(token) - - return token - - async def sign_up(self, username: str, password: str) -> str: - """ - Sign up a user to the database. - - :param password: the password to sign up with - :param username: the username to sign up with - - :return: str - """ - token = await self.__connection.send( - METHOD_SIGN_UP, {"user": username, "pass": password} - ) - self.__connection.set_token(token) - - return token - - async def authenticate(self, token: str) -> None: - """ - Authenticates a JWT. - - :param token: the JWT to authenticate - :return: None - """ - await self.__connection.send(METHOD_AUTHENTICATE, token) - self.__connection.set_token(token) - - async def invalidate(self, token: str) -> None: - """ - Invalidates a valid JWT. - - :param token: the JWT to invalidate - :return: None - """ - await self.__connection.send(METHOD_INVALIDATE, token) - self.__connection.set_token() - - async def info(self) -> dict: - """ - This returns the record of an authenticated record user. - - :return: dict - """ - return await self.__connection.send(METHOD_INFO) - - async def version(self) -> str: - """ - This returns the version of the Server backend. - - :return: str - """ - return await self.__connection.send(METHOD_VERSION) - - async def set(self, name: str, value) -> None: - await self.__connection.set(name, value) - - async def unset(self, name: str) -> None: - await self.__connection.unset(name) - - async def select( - self, what: Union[str, Table, RecordID] - ) -> Union[List[dict], dict]: - """ - Performs a select query on the database for a particular resource. - - :param what: the resource to select from. - - :return: the result of the select - """ - return await self.__connection.send(METHOD_SELECT, what) - - async def query(self, query: str, variables: dict = {}) -> List[QueryResponse]: - """ - Queries sends a custom SurrealQL query. - - :param query: The query to execute against SurrealDB. Queries are seperated by semicolons. - :param variables: A set of variables used by the query - - :return: An array of query results - """ - return await self.__connection.send(METHOD_QUERY, query, variables) - - async def create( - self, thing: Union[str, RecordID, Table], data: Union[List[dict], dict] - ): - """ - Creates a record either with a random or specified ID - - :param thing: The Table or Record ID to create. Passing just a table will result in a randomly generated ID - :param data: The data to store in the document - - :return: None - """ - return await self.__connection.send(METHOD_CREATE, thing, data) - - async def insert(self, thing: Union[str, Table], data: Union[List[dict], dict]): - """ - Inserts a record either with a random or specified ID. - - :param thing: The table to insert in to - :param data: One or multiple record(s) - :return: - """ - return await self.__connection.send(METHOD_INSERT, thing, data) - - async def patch( - self, - thing: Union[str, RecordID, Table], - patches: List[Patch], - diff: bool = False, - ): - """ - Patches the given resource with the given data. - - :param thing: The Table or Record ID to patch. - :param patches: An array of patches following the JSON Patch specification - :param diff: A boolean representing if just a diff should be returned. - :return: the patched resource such as a record/records or patches - """ - if diff is None: - diff = False - return await self.__connection.send(METHOD_PATCH, thing, patches, diff) - - async def update(self, thing: Union[str, RecordID, Table], data: dict): - """ - Updates replaces either all records in a table or a single record with specified data - - :param thing: The Table or Record ID to update. - :param data: The content for the record - :return: the updated resource such as an individual row or a list of rows - """ - return await self.__connection.send(METHOD_UPDATE, thing, data) - - async def upsert(self, thing: Union[str, RecordID, Table], data: dict): - """ - Upsert replaces either all records in a table or a single record with specified data - - :param thing: The Table or Record ID to upsert. - :param data: The content for the record - :return: the upsert-ed records such as an individual row or a list of rows - """ - return await self.__connection.send(METHOD_UPSERT, thing, data) - - async def delete( - self, thing: Union[str, RecordID, Table] - ) -> Union[List[dict], dict]: - """ - Deletes either all records in a table or a single record. - - :param thing: The Table or Record ID to update. - - :return: the record or records that were deleted - """ - return await self.__connection.send(METHOD_DELETE, thing) - - async def merge( - self, thing: Union[str, RecordID, Table], data: dict - ) -> Union[List[dict], dict]: - """ - Merge specified data into either all records in a table or a single record - - :param thing: The Table or Record ID to merge into. - :param data: The content for the record. - :return: the updated resource such as an individual row or a list of rows - """ - return await self.__connection.send(METHOD_MERGE, thing, data) - - async def live(self, thing: Union[str, Table], diff: bool = False) -> uuid.UUID: - """ - Live initiates a live query for a specified table name. - - :param thing: The Table tquery. - :param diff: If set to true, live notifications will contain an array of JSON Patches instead of the entire record - :return: the live query uuid - """ - return await self.__connection.send(METHOD_LIVE, thing, diff) - - async def live_notifications(self, live_id: uuid.UUID) -> asyncio.Queue: - """ - Live notification returns a queue that receives notification messages from the back end. - - :param live_id: The live id for the live query - :return: the notification queue - """ - return await self.__connection.live_notifications(live_id) - - async def kill(self, live_query_id: uuid.UUID) -> None: - """ - This kills an active live query - - :param live_query_id: The UUID of the live query to kill. - """ - - return await self.__connection.send(METHOD_KILL, live_query_id) diff --git a/surrealdb/asyncio_runtime.py b/surrealdb/asyncio_runtime.py deleted file mode 100644 index 4ba4fd3f..00000000 --- a/surrealdb/asyncio_runtime.py +++ /dev/null @@ -1,30 +0,0 @@ -import asyncio -from typing import Any - - -class AsyncController(type): - _instances: dict[Any, Any] = {} - - def __call__(cls, *args, **kwargs): # noqa: D102 - if cls not in cls._instances: - instance = super().__call__(*args, **kwargs) - cls._instances[cls] = instance - return cls._instances[cls] - - -class AsyncioRuntime(metaclass=AsyncController): - """ - The AsyncioRuntime class is a singleton class that is responsible for - managing the asyncio event loop and running the SurrealDB instance. - """ - - def __init__(self) -> None: - """The constructor for the AsyncioRuntime class.""" - self.loop = self._init_runtime() - - @staticmethod - def _init_runtime(): - """Defines the asyncio event loop.""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - return loop diff --git a/surrealdb/connection.py b/surrealdb/connection.py deleted file mode 100644 index 81441db1..00000000 --- a/surrealdb/connection.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -Defines the base Connection class for sending and receiving requests. -""" - -import logging -import secrets -import string -import threading -import uuid - -from dataclasses import dataclass -from typing import Dict, Tuple -from surrealdb.constants import ( - REQUEST_ID_LENGTH, - METHOD_SELECT, - METHOD_CREATE, - METHOD_INSERT, - METHOD_PATCH, - METHOD_UPDATE, - METHOD_UPSERT, - METHOD_DELETE, - METHOD_MERGE, - METHOD_LIVE, -) -from asyncio import Queue -from surrealdb.data.models import table_or_record_id - - -class ResponseType: - """ - Enum-like class representing response types for the connection. - - Attributes: - SEND (int): Response type for standard requests. - NOTIFICATION (int): Response type for notifications. - ERROR (int): Response type for errors. - """ - - SEND = 1 - NOTIFICATION = 2 - ERROR = 3 - - -@dataclass -class RequestData: - """ - Represents the data for a request sent over the connection. - - Attributes: - id (str): Unique identifier for the request. - method (str): The method name to invoke. - params (Tuple): Parameters for the method. - """ - - id: str - method: str - params: Tuple - - -class Connection: - """ - Base class for managing a connection to the database. - - Manages request/response lifecycle, including the use of queues for - handling asynchronous communication. - - Attributes: - _queues (Dict[int, dict]): Mapping of response types to their queues. - _namespace (str | None): Current namespace in use. - _database (str | None): Current database in use. - _auth_token (str | None): Authentication token. - """ - - _queues: Dict[int, dict] - _locks: Dict[int, threading.Lock] - _namespace: str | None = None - _database: str | None = None - _auth_token: str | None = None - - def __init__( - self, - base_url: str, - logger: logging.Logger, - encoder, - decoder, - timeout: int, - ): - """ - Initialize the Connection instance. - - Args: - base_url (str): The base URL of the server. - logger (logging.Logger): Logger for debugging and tracking activities. - encoder (function): Function to encode the request. - decoder (function): Function to decode the response. - timeout (int): Request timeout in seconds - """ - self._encoder = encoder - self._decoder = decoder - - self._locks = { - ResponseType.SEND: threading.Lock(), - ResponseType.NOTIFICATION: threading.Lock(), - ResponseType.ERROR: threading.Lock(), - } - self._queues = { - ResponseType.SEND: dict(), - ResponseType.NOTIFICATION: dict(), - ResponseType.ERROR: dict(), - } - - self._base_url = base_url - self._logger = logger - self._timeout = timeout - - async def use(self, namespace: str, database: str) -> None: - """ - Set the namespace and database for subsequent operations. - - Args: - namespace (str): The namespace to use. - database (str): The database to use. - """ - raise NotImplementedError("use method must be implemented") - - async def connect(self) -> None: - """ - Establish a connection to the server. - """ - raise NotImplementedError("connect method must be implemented") - - async def close(self) -> None: - """ - Close the connection to the server. - """ - raise NotImplementedError("close method must be implemented") - - async def _make_request(self, request_data: RequestData): - """ - Internal method to send a request and handle the response. - Args: - request_data (RequestData): The data to send. - return: - dict: The response data from the request. - """ - raise NotImplementedError("_make_request method must be implemented") - - async def set(self, key: str, value) -> None: - """ - Set a key-value pair in the database. - - Args: - key (str): The key to set. - value: The value to set. - """ - raise NotImplementedError("set method must be implemented") - - async def unset(self, key: str) -> None: - """ - Unset a key-value pair in the database. - - Args: - key (str): The key to unset. - """ - raise NotImplementedError("unset method must be implemented") - - def set_token(self, token: str | None = None) -> None: - """ - Set the authentication token for the connection. - - Args: - token (str): The authentication token to be set - """ - self._auth_token = token - - def create_response_queue(self, response_type: int, queue_id: str) -> Queue: - """ - Create a response queue for a given response type. - - Args: - response_type (int): The response type for the queue (1: SEND, 2: NOTIFICATION, 3: ERROR). - queue_id (str): The unique identifier for the queue. - Returns: - Queue: The response queue for the given response type and queue ID - (existing queues will be overwritten if same ID is used, cannot get existing queue). - """ - lock = self._locks[response_type] - with lock: - response_type_queues = self._queues.get(response_type) - if response_type_queues is None: - response_type_queues = {} - - queue = response_type_queues.get(queue_id) - if queue is None: - queue = Queue(maxsize=0) - response_type_queues[queue_id] = queue - self._queues[response_type] = response_type_queues - - return queue - - def get_response_queue(self, response_type: int, queue_id: str) -> Queue | None: - """ - Get a response queue for a given response type. - - Args: - response_type (int): The response type for the queue (1: SEND, 2: NOTIFICATION, 3: ERROR). - queue_id (str): The unique identifier for the queue. - - Returns: - Queue: The response queue for the given response type and queue ID - (existing queues will be overwritten if same ID is used). - """ - lock = self._locks[response_type] - with lock: - response_type_queues = self._queues.get(response_type) - if not response_type_queues: - return None - return response_type_queues.get(queue_id) - - def remove_response_queue(self, response_type: int, queue_id: str) -> None: - """ - Remove a response queue for a given response type. - - Notes: - Does not alert if the key is missing - - Args: - response_type (int): The response type for the queue (1: SEND, 2: NOTIFICATION, 3: ERROR). - queue_id (str): The unique identifier for the queue. - """ - lock = self._locks[response_type] - with lock: - response_type_queues = self._queues.get(response_type) - if response_type_queues: - response_type_queues.pop(queue_id, None) - - @staticmethod - def _prepare_method_params(method: str, params) -> Tuple: - prepared_params = params - if method in [ - METHOD_SELECT, - METHOD_CREATE, - METHOD_INSERT, - METHOD_PATCH, - METHOD_UPDATE, - METHOD_UPSERT, - METHOD_DELETE, - METHOD_MERGE, - METHOD_LIVE, - ]: # The first parameters for these methods are expected to be record id or table - if len(prepared_params) > 0 and isinstance(prepared_params[0], str): - thing = table_or_record_id(prepared_params[0]) - prepared_params = prepared_params[:0] + (thing,) + prepared_params[1:] - return prepared_params - - async def send(self, method: str, *params): - """ - Sends a request to the server with a unique ID and returns the response. - - Args: - method (str): The method of the request. - params: Parameters for the request. - - Returns: - dict: The response data from the request. - """ - - prepared_params = self._prepare_method_params(method, params) - request_data = RequestData( - id=request_id(REQUEST_ID_LENGTH), method=method, params=prepared_params - ) - self._logger.debug(f"Request {request_data.id}:", request_data) - - try: - result = await self._make_request(request_data) - - self._logger.debug(f"Result {request_data.id}:", result) - self._logger.debug( - "----------------------------------------------------------------------------------" - ) - - return result - except Exception as e: - self._logger.debug(f"Error {request_data.id}:", e) - self._logger.debug( - "----------------------------------------------------------------------------------" - ) - raise e - - async def live_notifications(self, live_query_id: uuid.UUID) -> Queue: - """ - Create a response queue for live notifications by essentially creating a NOTIFICATION response queue. - - Args: - live_query_id (uuid.UUID): The unique identifier for the live query. - - Returns: - Queue: The response queue for the live notifications. - """ - queue = self.create_response_queue( - ResponseType.NOTIFICATION, str(live_query_id) - ) - return queue - - -def request_id(length: int) -> str: - return "".join( - secrets.choice(string.ascii_letters + string.digits) for i in range(length) - ) diff --git a/surrealdb/connection_clib.py b/surrealdb/connection_clib.py deleted file mode 100644 index f2ce6960..00000000 --- a/surrealdb/connection_clib.py +++ /dev/null @@ -1,233 +0,0 @@ -import logging -import os -import ctypes -import platform - -from surrealdb.errors import SurrealDbConnectionError -from surrealdb.connection import Connection, RequestData -from surrealdb.constants import CLIB_FOLDER_PATH, METHOD_USE, METHOD_SET, METHOD_UNSET - - -def get_lib_path() -> str: - if platform.system() == "Linux": - lib_extension = ".so" - elif platform.system() == "Darwin": - lib_extension = ".dylib" - elif platform.system() == "Windows": - lib_extension = ".dll" - else: - raise SurrealDbConnectionError("Unsupported operating system") - - lib_path = os.path.join(CLIB_FOLDER_PATH, f"libsurrealdb_c{lib_extension}") - if os.path.isfile(lib_path) is not True: - raise Exception(f"{lib_path} is missing") - - return lib_path - - -class sr_string_t(ctypes.c_char_p): - pass - - -class sr_option_t(ctypes.Structure): - _fields_ = [ - ("strict", ctypes.c_bool), - ("query_timeout", ctypes.c_uint8), - ("transaction_timeout", ctypes.c_uint8), - ] - - -class sr_surreal_rpc_t(ctypes.Structure): - pass - - -class sr_RpcStream(ctypes.Structure): - pass - - -class sr_uuid_t(ctypes.Structure): - _fields_ = [("_0", ctypes.c_uint8 * 16)] - - -class sr_value_t_Tag(ctypes.c_int): - SR_VALUE_NONE = 0 - SR_VALUE_NULL = 1 - SR_VALUE_BOOL = 2 - SR_VALUE_NUMBER = 3 - SR_VALUE_STRAND = 4 - SR_VALUE_DURATION = 5 - SR_VALUE_DATETIME = 6 - SR_VALUE_UUID = 7 - SR_VALUE_ARRAY = 8 - SR_VALUE_OBJECT = 9 - SR_VALUE_BYTES = 10 - SR_VALUE_THING = 11 - - -class sr_value_t(ctypes.Structure): - _fields_ = [("tag", sr_value_t_Tag), ("sr_value_bool", ctypes.c_bool)] - - -class sr_notification_t(ctypes.Structure): - _fields_ = [ - ("query_id", sr_uuid_t), - ("action", ctypes.c_int), # sr_action - ("data", sr_value_t), - ] - - -class CLibConnection(Connection): - def __init__( - self, base_url: str, logger: logging.Logger, encoder, decoder, timeout: int - ): - super().__init__(base_url, logger, encoder, decoder, timeout) - - lib_path = get_lib_path() - self._lib = ctypes.CDLL(lib_path) - - self._c_surreal_rpc = None - self._c_surreal_stream = None - - def set_up_lib(self): - # int sr_surreal_rpc_new( - # sr_string_t *err_ptr, - # struct sr_surreal_rpc_t **surreal_ptr, - # const char *endpoint, - # struct sr_option_t options); - self._lib.sr_surreal_rpc_new.argtypes = [ - ctypes.POINTER(sr_string_t), # sr_string_t *err_ptr - ctypes.POINTER( - ctypes.POINTER(sr_surreal_rpc_t) - ), # struct sr_surreal_rpc_t **surreal_ptr - ctypes.c_char_p, # const char *endpoint - sr_option_t, # struct sr_option_t options - ] - self._lib.sr_surreal_rpc_new.restype = ctypes.c_int - - # int sr_surreal_rpc_notifications( - # const struct sr_surreal_rpc_t *self, - # sr_string_t *err_ptr, - # struct sr_RpcStream **stream_ptr); - self._lib.sr_surreal_rpc_notifications.argtypes = [ - ctypes.POINTER(sr_surreal_rpc_t), # const sr_surreal_rpc_t *self - ctypes.POINTER(sr_string_t), # sr_string_t *err_ptr - ctypes.POINTER( - ctypes.POINTER(sr_RpcStream) - ), # struct sr_RpcStream **stream_ptr - ] - self._lib.sr_surreal_rpc_notifications.restype = ctypes.c_int - - # int sr_surreal_rpc_execute( - # const struct sr_surreal_rpc_t *self, - # sr_string_t *err_ptr, - # uint8_t **res_ptr, - # const uint8_t *ptr, - # int len - # ); - self._lib.sr_surreal_rpc_execute.argtypes = [ - ctypes.POINTER(sr_surreal_rpc_t), # const sr_surreal_rpc_t *self - ctypes.POINTER(sr_string_t), # sr_string_t *err_ptr - ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), # uint8_t **res_ptr - ctypes.POINTER(ctypes.c_uint8), # const uint8_t *ptr - ctypes.c_int, # int len - ] - self._lib.sr_surreal_rpc_execute.restype = ctypes.c_int - - # sr_stream_next - self._lib.sr_stream_next.argtypes = [ - ctypes.POINTER(sr_RpcStream), # const sr_stream_t *self - ctypes.POINTER(sr_notification_t), # sr_notification_t *notification_ptr - ] - self._lib.sr_stream_next.restype = ctypes.c_int - - # sr_stream_kill - self._lib.sr_stream_kill.argtypes = [ctypes.POINTER(sr_RpcStream)] - self._lib.sr_stream_kill.restype = None - - # sr_free_string - self._lib.sr_free_string.argtypes = [sr_string_t] - self._lib.sr_free_string.restype = None - - # sr_free_byte_arr - self._lib.sr_free_byte_arr.argtypes = [ - ctypes.POINTER(ctypes.c_uint8), - ctypes.c_int, - ] - self._lib.sr_free_byte_arr.restype = None - - async def connect(self): - c_err = sr_string_t() - c_rpc_connection = ctypes.POINTER(sr_surreal_rpc_t)() - c_endpoint = bytes(self._base_url, "utf-8") - c_options = sr_option_t(strict=True, query_timeout=10, transaction_timeout=20) - - try: - if ( - self._lib.sr_surreal_rpc_new( - ctypes.byref(c_err), - ctypes.byref(c_rpc_connection), - c_endpoint, - c_options, - ) - < 0 - ): - raise SurrealDbConnectionError( - f"Error connecting to RPC. {c_err.value.decode()}" - ) - self._c_surreal_rpc = c_rpc_connection - - except Exception as e: - raise SurrealDbConnectionError("cannot connect db server", e) - finally: - self._lib.sr_free_string(c_err) - - async def close(self): - self._lib.sr_surreal_rpc_free(self._c_surreal_rpc) - - async def use(self, namespace: str, database: str) -> None: - self._namespace = namespace - self._database = database - - await self.send(METHOD_USE, namespace, database) - - async def set(self, key: str, value): - await self.send(METHOD_SET, key, value) - - async def unset(self, key: str): - await self.send(METHOD_UNSET, key) - - async def _make_request(self, request_data: RequestData): - request_payload = self._encoder( - { - "id": request_data.id, - "method": request_data.method, - "params": request_data.params, - } - ) - - c_err = sr_string_t() - c_res_ptr = ctypes.POINTER(ctypes.c_uint8)() - payload_len = len(request_payload) - - # Call sr_surreal_rpc_execute - result = self._lib.sr_surreal_rpc_execute( - self._c_surreal_rpc, - ctypes.byref(c_err), - ctypes.byref(c_res_ptr), - (ctypes.c_uint8 * payload_len)(*request_payload), - payload_len, - ) - - if result < 0: - raise SurrealDbConnectionError( - f"Error executing RPC: {c_err.value.decode() if c_err.value else 'Unknown error'}" - ) - - # Convert the result pointer to a Python byte array - response = ctypes.string_at(c_res_ptr, result) - - # Free the allocated byte array returned by the C library - self._lib.sr_free_byte_arr(c_res_ptr, result) - response_data = self._decoder(response) - - return response_data diff --git a/surrealdb/connection_factory.py b/surrealdb/connection_factory.py deleted file mode 100644 index ca07f3ca..00000000 --- a/surrealdb/connection_factory.py +++ /dev/null @@ -1,63 +0,0 @@ -import logging - -from urllib.parse import urlparse - -from surrealdb.connection import Connection -from surrealdb.constants import ( - ALLOWED_CONNECTION_SCHEMES, - WS_CONNECTION_SCHEMES, - HTTP_CONNECTION_SCHEMES, - CLIB_CONNECTION_SCHEMES, - DEFAULT_REQUEST_TIMEOUT, - DEFAULT_CONNECTION_URL, -) -from surrealdb.connection_clib import CLibConnection -from surrealdb.connection_http import HTTPConnection -from surrealdb.connection_ws import WebsocketConnection -from surrealdb.data.cbor import encode, decode -from surrealdb.errors import SurrealDbConnectionError - - -def create_connection_factory( - connection_url: str | None, - logger: logging.Logger, - timeout: int = DEFAULT_REQUEST_TIMEOUT, -) -> Connection: - if logger is None: - logger = logging.getLogger(__name__) - - if connection_url is None: - connection_url = DEFAULT_CONNECTION_URL - - if timeout <= 0: - timeout = DEFAULT_REQUEST_TIMEOUT - - parsed_url = urlparse(connection_url) - if parsed_url.scheme not in ALLOWED_CONNECTION_SCHEMES: - raise SurrealDbConnectionError( - "invalid scheme. allowed schemes are", "".join(ALLOWED_CONNECTION_SCHEMES) - ) - - if parsed_url.scheme in WS_CONNECTION_SCHEMES: - logger.debug("websocket url detected, creating a websocket connection") - return WebsocketConnection( - connection_url, logger, encoder=encode, decoder=decode, timeout=timeout - ) - - if parsed_url.scheme in HTTP_CONNECTION_SCHEMES: - logger.debug("http url detected, creating a http connection") - return HTTPConnection( - connection_url, logger, encoder=encode, decoder=decode, timeout=timeout - ) - - if parsed_url.scheme in CLIB_CONNECTION_SCHEMES: - logger.debug("embedded url detected, creating a clib connection") - clib_url = connection_url - if parsed_url.scheme == "mem": - clib_url = urlparse(connection_url, "memory").geturl() - - return CLibConnection( - clib_url, logger, encoder=encode, decoder=decode, timeout=timeout - ) - - raise Exception("no connection type available") diff --git a/surrealdb/connection_http.py b/surrealdb/connection_http.py deleted file mode 100644 index fd08b71d..00000000 --- a/surrealdb/connection_http.py +++ /dev/null @@ -1,93 +0,0 @@ -import threading -from typing import Any, Tuple - -import requests - -from surrealdb.connection import Connection, RequestData -from surrealdb.errors import SurrealDbConnectionError - - -class HTTPConnection(Connection): - _request_variables: dict[str, Any] = {} - _request_variables_lock = threading.Lock() - _is_ready: bool = False - - async def use(self, namespace: str, database: str) -> None: - self._namespace = namespace - self._database = database - - async def set(self, key: str, value): - with self._request_variables_lock: - self._request_variables[key] = value - - async def unset(self, key: str): - with self._request_variables_lock: - if self._request_variables.get(key) is not None: - del self._request_variables[key] - - async def connect(self) -> None: - if self._base_url is None: - raise SurrealDbConnectionError("base url not set for http connection") - - response = requests.get(self._base_url + "/health") - if response.status_code != 200: - self._logger.debug("HTTP health check successful") - raise SurrealDbConnectionError( - "connection failed. check server is up and base url is correct" - ) - self._is_ready = True - - async def close(self): - self._is_ready = False - - def _prepare_query_method_params(self, params: Tuple) -> Tuple: - query, variables = params - variables = ( - {**variables, **self._request_variables} - if variables - else self._request_variables.copy() - ) - return query, variables - - async def _make_request(self, request_data: RequestData): - if not self._is_ready: - raise SurrealDbConnectionError( - "connection not ready. Call the connect() method first" - ) - - if self._namespace is None: - raise SurrealDbConnectionError("namespace not set") - - if self._database is None: - raise SurrealDbConnectionError("database not set") - - headers = { - "Content-Type": "application/cbor", - "Accept": "application/cbor", - "Surreal-NS": self._namespace, - "Surreal-DB": self._database, - } - - if self._auth_token is not None: - headers["Authorization"] = f"Bearer {self._auth_token}" - - if request_data.method.lower() == "query": - request_data.params = self._prepare_query_method_params(request_data.params) - - request_payload = self._encoder( - { - "id": request_data.id, - "method": request_data.method, - "params": request_data.params, - } - ) - - response = requests.post( - f"{self._base_url}/rpc", data=request_payload, headers=headers - ) - response_data = self._decoder(response.content) - - if 200 > response.status_code > 299 or response_data.get("error"): - raise SurrealDbConnectionError(response_data.get("error").get("message")) - - return response_data.get("result") diff --git a/surrealdb/connection_ws.py b/surrealdb/connection_ws.py deleted file mode 100644 index 0e98efb5..00000000 --- a/surrealdb/connection_ws.py +++ /dev/null @@ -1,115 +0,0 @@ -import asyncio -from asyncio import Task - -from websockets import Subprotocol, ConnectionClosed, connect -from websockets.asyncio.client import ClientConnection - -from surrealdb.connection import Connection, ResponseType, RequestData -from surrealdb.constants import METHOD_USE, METHOD_SET, METHOD_UNSET -from surrealdb.errors import SurrealDbConnectionError - - -class WebsocketConnection(Connection): - _ws: ClientConnection - _receiver_task: Task - - async def connect(self): - try: - self._ws = await connect( - self._base_url + "/rpc", - subprotocols=[Subprotocol("cbor")], - max_size=1048576, - ) - self._receiver_task = asyncio.create_task(self._listen_to_ws(self._ws)) - except Exception as e: - raise SurrealDbConnectionError("cannot connect db server", e) - - async def use(self, namespace: str, database: str) -> None: - self._namespace = namespace - self._database = database - - await self.send(METHOD_USE, namespace, database) - - async def set(self, key: str, value) -> None: - """ - Set a key-value pair in the database. - - Args: - key (str): The key to set. - value: The value to set. - """ - await self.send(METHOD_SET, key, value) - - async def unset(self, key: str): - """ - Unset a key-value pair in the database. - - Args: - key (str): The key to unset. - """ - await self.send(METHOD_UNSET, key) - - async def close(self): - if self._receiver_task and not self._receiver_task.cancelled(): - self._receiver_task.cancel() - - if self._ws: - await self._ws.close() - - async def _make_request(self, request_data: RequestData): - request_payload = self._encoder( - { - "id": request_data.id, - "method": request_data.method, - "params": request_data.params, - } - ) - - try: - queue = self.create_response_queue(ResponseType.SEND, request_data.id) - await self._ws.send(request_payload) - - response_data = await asyncio.wait_for( - queue.get(), self._timeout - ) # Set timeout - - if response_data.get("error"): - raise SurrealDbConnectionError( - response_data.get("error").get("message") - ) - return response_data.get("result") - - except ConnectionClosed as e: - raise SurrealDbConnectionError(e) - except asyncio.TimeoutError: - raise SurrealDbConnectionError( - f"Request timed-out after {self._timeout} seconds" - ) - except Exception as e: - raise e - finally: - self.remove_response_queue(ResponseType.SEND, request_data.id) - - async def _listen_to_ws(self, ws): - async for message in ws: - try: - response_data = self._decoder(message) - - response_id = response_data.get("id") - if response_id: - queue = self.get_response_queue(ResponseType.SEND, response_id) - await queue.put(response_data) - continue - - live_id = response_data.get("result").get("id") # returned as uuid - queue = self.get_response_queue(ResponseType.NOTIFICATION, str(live_id)) - if queue is None: - self._logger.error(f"No notification queue set for {live_id}") - continue - await queue.put(response_data.get("result")) - except asyncio.CancelledError: - self._logger.info("Task cancelled. Stopped listening for RPC responses") - break - except Exception as e: - self._logger.error(e) - continue diff --git a/surrealdb/constants.py b/surrealdb/constants.py deleted file mode 100644 index 1e61ed7d..00000000 --- a/surrealdb/constants.py +++ /dev/null @@ -1,46 +0,0 @@ -import os - -REQUEST_ID_LENGTH = 10 - -# methods -METHOD_USE = "use" -METHOD_SIGN_IN = "signin" -METHOD_SIGN_UP = "signup" -METHOD_INFO = "info" -METHOD_VERSION = "version" -METHOD_AUTHENTICATE = "authenticate" -METHOD_INVALIDATE = "invalidate" -METHOD_SET = "let" -METHOD_UNSET = "unset" -METHOD_SELECT = "select" -METHOD_QUERY = "query" -METHOD_CREATE = "create" -METHOD_INSERT = "insert" -METHOD_PATCH = "patch" -METHOD_MERGE = "merge" -METHOD_UPDATE = "update" -METHOD_UPSERT = "upsert" -METHOD_DELETE = "delete" -METHOD_LIVE = "live" -METHOD_KILL = "kill" - -# Connection -HTTP_CONNECTION_SCHEMES = ["http", "https"] -WS_CONNECTION_SCHEMES = ["ws", "wss"] -CLIB_CONNECTION_SCHEMES = ["memory", "surrealkv", "mem"] -ALLOWED_CONNECTION_SCHEMES = ( - HTTP_CONNECTION_SCHEMES + WS_CONNECTION_SCHEMES + CLIB_CONNECTION_SCHEMES -) - -DEFAULT_CONNECTION_URL = "http://127.0.0.1:8000" - -# Methods -UNSUPPORTED_HTTP_METHODS = ["kill", "live"] - -# Paths -ROOT_DIR = os.path.abspath( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") -) -CLIB_FOLDER_PATH = os.path.join(ROOT_DIR, "libsrc") - -DEFAULT_REQUEST_TIMEOUT = 10 # seconds diff --git a/surrealdb/data/types/datetime.py b/surrealdb/data/types/datetime.py deleted file mode 100644 index 53526d96..00000000 --- a/surrealdb/data/types/datetime.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime -from typing import Tuple - -import pytz # type: ignore -from math import floor - - -@dataclass -class DateTimeCompact: - timestamp: int = 0 # nanoseconds - - @staticmethod - def parse(seconds: int, nanoseconds: int) -> "DateTimeCompact": - return DateTimeCompact(nanoseconds + (seconds * pow(10, 9))) - - def get_seconds_and_nano(self) -> Tuple[int, int]: - sec = floor(self.timestamp / pow(10, 9)) - nsec = self.timestamp - (sec * pow(10, 9)) - - return sec, nsec - - def get_date_time(self, fmt: str = "%Y-%m-%dT%H:%M:%S.%fZ"): - return datetime.fromtimestamp(self.timestamp / pow(10, 9), pytz.UTC).strftime( - fmt - ) diff --git a/surrealdb/data/types/duration.py b/surrealdb/data/types/duration.py deleted file mode 100644 index 09b94025..00000000 --- a/surrealdb/data/types/duration.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - -from math import floor - - -@dataclass -class Duration: - elapsed: int = 0 # nanoseconds - - @staticmethod - def parse(seconds: int, nanoseconds: int): - return Duration(nanoseconds + (seconds * pow(10, 9))) - - def get_seconds_and_nano(self) -> Tuple[int, int]: - sec = floor(self.elapsed / pow(10, 9)) - nsec = self.elapsed - (sec * pow(10, 9)) - - return sec, nsec diff --git a/surrealdb/data/types/future.py b/surrealdb/data/types/future.py deleted file mode 100644 index 2a5e0881..00000000 --- a/surrealdb/data/types/future.py +++ /dev/null @@ -1,7 +0,0 @@ -from dataclasses import dataclass -from typing import Any - - -@dataclass -class Future: - value: Any diff --git a/surrealdb/data/types/geometry.py b/surrealdb/data/types/geometry.py deleted file mode 100644 index 3cf5575f..00000000 --- a/surrealdb/data/types/geometry.py +++ /dev/null @@ -1,132 +0,0 @@ -from dataclasses import dataclass -from typing import List, Tuple - - -class Geometry: - def get_coordinates(self): - pass - - @staticmethod - def parse_coordinates(coordinates): - pass - - -@dataclass -class GeometryPoint(Geometry): - longitude: float - latitude: float - - def __repr__(self): - return f"{self.__class__.__name__}(longitude={self.longitude}, latitude={self.latitude})".format( - self=self - ) - - def get_coordinates(self) -> Tuple[float, float]: - return self.longitude, self.latitude - - @staticmethod - def parse_coordinates(coordinates): - return GeometryPoint(coordinates[0], coordinates[1]) - - -@dataclass -class GeometryLine(Geometry): - - def __init__( - self, point1: GeometryPoint, point2: GeometryPoint, *other_points: GeometryPoint - ): - self.geometry_points = [point1, point2] + list(other_points) - - def get_coordinates(self) -> List[Tuple[float, float]]: - return [point.get_coordinates() for point in self.geometry_points] - - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_points)})' - - @staticmethod - def parse_coordinates(coordinates): - return GeometryLine( - *[GeometryPoint.parse_coordinates(point) for point in coordinates] - ) - - -@dataclass -class GeometryPolygon(Geometry): - def __init__(self, line1, line2, *other_lines: GeometryLine): - self.geometry_lines = [line1, line2] + list(other_lines) - - def get_coordinates(self) -> List[List[Tuple[float, float]]]: - return [line.get_coordinates() for line in self.geometry_lines] - - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_lines)})' - - @staticmethod - def parse_coordinates(coordinates): - return GeometryPolygon( - *[GeometryLine.parse_coordinates(line) for line in coordinates] - ) - - -@dataclass -class GeometryMultiPoint(Geometry): - def __init__(self, *geometry_points: GeometryPoint): - self.geometry_points = geometry_points - - def get_coordinates(self) -> List[Tuple[float, float]]: - return [point.get_coordinates() for point in self.geometry_points] - - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_points)})' - - @staticmethod - def parse_coordinates(coordinates): - return GeometryMultiPoint( - *[GeometryPoint.parse_coordinates(point) for point in coordinates] - ) - - -@dataclass -class GeometryMultiLine(Geometry): - def __init__(self, *geometry_lines: GeometryLine): - self.geometry_lines = geometry_lines - - def get_coordinates(self) -> List[List[Tuple[float, float]]]: - return [line.get_coordinates() for line in self.geometry_lines] - - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_lines)})' - - @staticmethod - def parse_coordinates(coordinates): - return GeometryMultiLine( - *[GeometryLine.parse_coordinates(line) for line in coordinates] - ) - - -@dataclass -class GeometryMultiPolygon(Geometry): - def __init__(self, *geometry_polygons: GeometryPolygon): - self.geometry_polygons = geometry_polygons - - def get_coordinates(self) -> List[List[List[Tuple[float, float]]]]: - return [polygon.get_coordinates() for polygon in self.geometry_polygons] - - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometry_polygons)})' - - @staticmethod - def parse_coordinates(coordinates): - return GeometryMultiPolygon( - *[GeometryPolygon.parse_coordinates(polygon) for polygon in coordinates] - ) - - -@dataclass() -class GeometryCollection: - - def __init__(self, *geometries: Geometry): - self.geometries = geometries - - def __repr__(self): - return f'{self.__class__.__name__}({", ".join(repr(geo) for geo in self.geometries)})' diff --git a/surrealdb/data/types/range.py b/surrealdb/data/types/range.py deleted file mode 100644 index 61f3d834..00000000 --- a/surrealdb/data/types/range.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass - - -class Bound: - def __init__(self): - pass - - -@dataclass -class BoundIncluded(Bound): - def __init__(self, value): - super().__init__() - self.value = value - - -@dataclass -class BoundExcluded(Bound): - def __init__(self, value): - super().__init__() - self.value = value - - -@dataclass -class Range: - begin: Bound - end: Bound diff --git a/surrealdb/data/types/record_id.py b/surrealdb/data/types/record_id.py deleted file mode 100644 index fb786f55..00000000 --- a/surrealdb/data/types/record_id.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class RecordID: - def __init__(self, table_name: str, identifier): - self.table_name = table_name - self.id = identifier - - def __str__(self) -> str: - return f"{self.table_name}:{self.id}" - - def __repr__(self) -> str: - return f"{self.__class__.__name__}(table_name={self.table_name}, record_id={self.id})".format( - self=self - ) - - @staticmethod - def parse(record_str: str): - if ":" not in record_str: - raise ValueError( - 'invalid string provided for parse. the expected string format is "table_name:record_id"' - ) - - table, record_id = record_str.split(":") - return RecordID(table, record_id) diff --git a/surrealdb/data/types/table.py b/surrealdb/data/types/table.py deleted file mode 100644 index dd940c3d..00000000 --- a/surrealdb/data/types/table.py +++ /dev/null @@ -1,9 +0,0 @@ -class Table: - def __init__(self, table_name: str): - self.table_name = table_name - - def __str__(self) -> str: - return f"{self.table_name}" - - def __repr__(self) -> str: - return f"{self.table_name}" diff --git a/surrealdb/errors.py b/surrealdb/errors.py deleted file mode 100644 index d5de7cc4..00000000 --- a/surrealdb/errors.py +++ /dev/null @@ -1,20 +0,0 @@ -class SurrealDbError(Exception): - """Base class for exceptions in this module.""" - - -class SurrealDbConnectionError(SurrealDbError): - """ - Exceptions from connections - """ - - -class SurrealDbDecodeError(SurrealDbError): - """ - Exceptions from Decoding responses - """ - - -class SurrealDbEncodeError(SurrealDbError): - """ - Exceptions from encoding requests - """ diff --git a/surrealdb/surrealdb.py b/surrealdb/surrealdb.py deleted file mode 100644 index 7a21c36d..00000000 --- a/surrealdb/surrealdb.py +++ /dev/null @@ -1,362 +0,0 @@ -""" -This file defines the interface for the AsyncSurrealDB. - -# Usage -The database can be used by the following code: -```python -from surrealdb.async_surrealdb import AsyncSurrealDB - -db = SurrealDB(url="ws://localhost:8080") -db.connect() -db.use("ns", "db_name") -``` -It can also be used as a context manager: -```python -from surrealdb.surrealdb import SurrealDB - -with SurrealDB("ws://localhost:8080") as db: - db.use("ns", "db_name") -``` -""" - -import asyncio -import logging -import uuid -from typing import Union, List -from typing_extensions import Self - -from surrealdb.asyncio_runtime import AsyncioRuntime -from surrealdb.constants import ( - METHOD_KILL, - METHOD_LIVE, - METHOD_MERGE, - METHOD_DELETE, - METHOD_UPSERT, - METHOD_UPDATE, - METHOD_PATCH, - METHOD_INSERT, - METHOD_CREATE, - METHOD_QUERY, - METHOD_SELECT, - METHOD_VERSION, - METHOD_INFO, - METHOD_INVALIDATE, - METHOD_AUTHENTICATE, - METHOD_SIGN_UP, - METHOD_SIGN_IN, -) -from surrealdb.connection_factory import create_connection_factory -from surrealdb.data import RecordID, Table, Patch - - -class SurrealDB: - """This class is responsible for managing the connection to SurrealDB and managing operations on the connection.""" - - def __init__(self, url: str | None = None, timeout: int = 0) -> None: - """ - The constructor for the SurrealDB class. - - :param url: the url to connect to SurrealDB with - """ - logger = logging.getLogger(__name__) - self.__connection = create_connection_factory( - connection_url=url, timeout=timeout, logger=logger - ) - - def __enter__(self): - self.connect() - return self - - def __exit__(self, *args): - self.close() - - def connect(self) -> Self: - """Connect to SurrealDB.""" - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete(self.__connection.connect()) - - return self - - def close(self): - """Close connection to SurrealDB.""" - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete(self.__connection.close()) - - def use(self, namespace: str, database: str) -> None: - """ - Uses the given namespace and database in the connection. - - :param namespace: the namespace to use - :param database: the database to use - :return: None - """ - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete( - self.__connection.use(namespace=namespace, database=database) - ) - - def sign_in(self, username: str, password: str) -> str: - """ - Signs in to the database. - - :param password: the password to sign in with - :param username: the username to sign in with - - :return: str - """ - - async def async_fn(_username: str, _password: str): - _token = await self.__connection.send( - METHOD_SIGN_IN, {"user": _username, "pass": _password} - ) - self.__connection.set_token(_token) - return _token - - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete(async_fn(username, password)) - - def sign_up(self, username: str, password: str) -> str: - """ - Sign up a user to the database. - - :param password: the password to sign up with - :param username: the username to sign up with - - :return: str - """ - - async def async_fn(_username: str, _password: str): - _token = await self.__connection.send( - METHOD_SIGN_UP, {"user": _username, "pass": _password} - ) - self.__connection.set_token(_token) - return _token - - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete(async_fn(username, password)) - - def authenticate(self, token: str) -> None: - """ - Authenticates a JWT. - - :param token: the JWT to authenticate - :return: None - """ - - async def async_fn(_jwt: str): - _token = await self.__connection.send(METHOD_AUTHENTICATE, _jwt) - self.__connection.set_token(_token) - return _token - - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete(async_fn(token)) - - def invalidate(self, token: str) -> None: - """ - Invalidates a valid JWT. - - :param token: the JWT to invalidate - :return: None - """ - - async def async_fn(_token: str): - await self.__connection.send(METHOD_INVALIDATE, _token) - self.__connection.set_token() - return _token - - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete(async_fn(token)) - - def info(self) -> dict: - """ - This returns the record of an authenticated record user. - - :return: dict - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete(self.__connection.send(METHOD_INFO)) - - def version(self) -> str: - """ - This returns the version of the Server backend. - - :return: str - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_VERSION) - ) - - def set(self, name: str, value) -> None: - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete(self.__connection.set(name, value)) - - def unset(self, name: str) -> None: - loop_manager = AsyncioRuntime() - loop_manager.loop.run_until_complete(self.__connection.unset(name)) - - def select(self, what: Union[str, Table, RecordID]) -> Union[List[dict], dict]: - """ - Performs a select query on the database for a particular resource. - - :param what: the resource to select from. - - :return: the result of the select - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_SELECT, what) - ) - - def query(self, query: str, variables: dict = {}) -> List[dict]: - """ - Queries sends a custom SurrealQL query. - - :param query: The query to execute against SurrealDB. Queries are seperated by semicolons. - :param variables: A set of variables used by the query - - :return: An array of query results - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_QUERY, query, variables) - ) - - def create(self, thing: Union[str, RecordID, Table], data: Union[List[dict], dict]): - """ - Creates a record either with a random or specified ID - - :param thing: The Table or Record ID to create. Passing just a table will result in a randomly generated ID - :param data: The data to store in the document - - :return: None - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_CREATE, thing, data) - ) - - def insert(self, thing: Union[str, Table], data: Union[List[dict], dict]): - """ - Inserts a record either with a random or specified ID. - - :param thing: The table to insert in to - :param data: One or multiple record(s) - :return: - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_INSERT, thing, data) - ) - - def patch( - self, - thing: Union[str, RecordID, Table], - patches: List[Patch], - diff: bool = False, - ): - """ - Patches the given resource with the given data. - - :param thing: The Table or Record ID to patch. - :param patches: An array of patches following the JSON Patch specification - :param diff: A boolean representing if just a diff should be returned. - :return: the patched resource such as a record/records or patches - """ - if diff is None: - diff = False - - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_PATCH, thing, patches, diff) - ) - - def update(self, thing: Union[str, RecordID, Table], data: dict): - """ - Updates replaces either all records in a table or a single record with specified data - - :param thing: The Table or Record ID to update. - :param data: The content for the record - :return: the updated resource such as an individual row or a list of rows - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_UPDATE, thing, data) - ) - - def upsert(self, thing: Union[str, RecordID, Table], data: dict): - """ - Upsert replaces either all records in a table or a single record with specified data - - :param thing: The Table or Record ID to upsert. - :param data: The content for the record - :return: the upsert-ed records such as an individual row or a list of rows - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_UPSERT, thing, data) - ) - - def delete(self, thing: Union[str, RecordID, Table]) -> Union[List[dict], dict]: - """ - Deletes either all records in a table or a single record. - - :param thing: The Table or Record ID to update. - - :return: the record or records that were deleted - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_DELETE, thing) - ) - - def merge( - self, thing: Union[str, RecordID, Table], data: dict - ) -> Union[List[dict], dict]: - """ - Merge specified data into either all records in a table or a single record - - :param thing: The Table or Record ID to merge into. - :param data: The content for the record. - :return: the updated resource such as an individual row or a list of rows - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_MERGE, thing, data) - ) - - def live(self, thing: Union[str, Table], diff: bool = False) -> uuid.UUID: - """ - Live initiates a live query for a specified table name. - - :param thing: The Table tquery. - :param diff: If set to true, live notifications will contain an array of JSON Patches instead of the entire record - :return: the live query uuid - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_LIVE, thing, diff) - ) - - def live_notifications(self, live_id: uuid.UUID) -> asyncio.Queue: - """ - Live notification returns a queue that receives notification messages from the back end. - - :param live_id: The live id for the live query - :return: the notification queue - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.live_notifications(live_id) - ) - - def kill(self, live_query_id: uuid.UUID) -> None: - """ - This kills an active live query - - :param live_query_id: The UUID of the live query to kill. - """ - loop_manager = AsyncioRuntime() - return loop_manager.loop.run_until_complete( - self.__connection.send(METHOD_KILL, live_query_id) - ) diff --git a/surrealdb/utils.py b/surrealdb/utils.py deleted file mode 100644 index d703ac31..00000000 --- a/surrealdb/utils.py +++ /dev/null @@ -1,8 +0,0 @@ -from urllib.parse import urlparse, urlunparse - - -def change_url_scheme(url, new_scheme): - parsed_url = urlparse(url) - modified_url = parsed_url._asdict() - modified_url["scheme"] = new_scheme - return urlunparse(modified_url.values()) diff --git a/tests/README.md b/tests/README.md deleted file mode 100644 index c18b426f..00000000 --- a/tests/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Testing - -This testing suite is mainly used to test the python SDK and how it runs against multiple different versions of -SurrealDB for the following variances: - -### Python versions - -- 3.7 -- 3.8 -- 3.9 -- 3.10 - -These all get tested for the last 4 versions of SurrealDB, with blocking and async clients, and websocket and http -protocols. At the time of writing this, it results in 960 tests but will quickly be increasing. - -## Atomic Testing -Python is an ideal choice for these tests due to Python not supporting multithreading through the GIL. This means -that we can only run one request to the database at a time even though we are running multiple tests. Each test -has a `setUp` and `tearDown` method as seen below: - -```python -def setUp(self): - self.connection = SurrealDB(Url().url) - self.queries: List[str] = [] - - self.connection.signin({ - "username": "root", - "password": "root", - }) - -def tearDown(self): - for query in self.queries: - self.connection.query(query) -``` -Here we are see that the `setUp` method creates a new connection to the database and signs in with the root user. The -`tearDown` method then runs all the queries that were run in the test at the start of a test so when the test finishes -or breaks, the teardown queries are run to clean up the database. This means that the state of the database is clear -at the start of every test no matter what order we run or if we run a test individually when developing a new feature. -An example test of the class having these setup and teardown methods is below: - -```python -def test_update_ql(self): - self.queries = ["DELETE user;"] - self.connection.query("CREATE user:tobie SET name = 'Tobie';") - self.connection.query("CREATE user:jaime SET name = 'Jaime';") - outcome = self.connection.query("UPDATE user SET lastname = 'Morgan Hitchcock';") - self.assertEqual( - [ - {'id': 'user:jaime', "lastname": "Morgan Hitchcock", 'name': 'Jaime'}, - {'id': 'user:tobie', "lastname": "Morgan Hitchcock", 'name': 'Tobie'} - ], - outcome - ) -``` -Here we can see that the `test_update_ql` method is setting the `self.queries` variable to `["DELETE user;"]` which -wipes the user table at the end of the test or if the test breaks. The test then creates two users, updates the -lastname of all users and then checks that the users have been updated correctly. This is a simple example but we can -run it as many times as we want and it will always work as expected. The test after this test might also do something -to the user table, but it will always be in the same state at the start of the test. - -## Different SurrealDB Versions -The tests are run against the last 4 versions of SurrealDB. This is to ensure that the SDK is always compatible with -the latest version of SurrealDB and that it is also backwards compatible. We can define the protocol and surrealDB -version based on the environment variables `CONNECTION_PROTOCOL` and `CONNECTION_PORT`. The default values are `http` -and `8000` which is the latest version so there is no need to set these values unless you want to test against a -different protocol or older version. You can run the tests with the following command: - -```bash -# testing all the http async and blocking for last five versions for http -export CONNECTION_PROTOCOL="http" -python -m unittest discover -export CONNECTION_PORT="8121" -python -m unittest discover -export CONNECTION_PORT="8120" -python -m unittest discover -export CONNECTION_PORT="8101" -python -m unittest discover -export CONNECTION_PORT="8111" -python -m unittest discover - -# testing all the ws async and blocking for last five versions for websocket -export CONNECTION_PROTOCOL="ws" -export CONNECTION_PORT="8000" -python -m unittest discover -export CONNECTION_PORT="8121" -python -m unittest discover -export CONNECTION_PORT="8120" -python -m unittest discover -export CONNECTION_PORT="8101" -python -m unittest discover -export CONNECTION_PORT="8111" -python -m unittest discover -``` diff --git a/tests/builds/big_data_dockerfile b/tests/builds/big_data_dockerfile deleted file mode 100644 index ffebe019..00000000 --- a/tests/builds/big_data_dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM debian:bullseye-slim - -RUN apt-get update && apt-get install -y libpq-dev -RUN apt-get install -y curl -RUN apt-get install libssl-dev -y -RUN apt-get install tree -y -RUN apt-get install -y --no-install-recommends unzip bash -RUN curl -sSL https://raw.githubusercontent.com/maxwellflitton/surrealdb-backup-cli/main/scripts/install.sh | bash - -COPY ./db_snapshots/big_data_snapshot/package/ ./big_data_snapshot/ -RUN ./surrealdb_backup unpack -d ./unpacked_data/ -t ./big_data_snapshot/ -RUN rm ./unpacked_data/LOCK -RUN curl -sSf https://install.surrealdb.com | sh - -EXPOSE 8000 - -CMD ["surreal", "start", "--bind", "0.0.0.0:8000", "--allow-all", "--user", "root", "--pass", "root", "rocksdb:./unpacked_data"] diff --git a/tests/integration/async/test_auth.py b/tests/integration/async/test_auth.py deleted file mode 100644 index f3ad328e..00000000 --- a/tests/integration/async/test_auth.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Handles the integration tests for logging into the database. -""" - -from unittest import IsolatedAsyncioTestCase, main - -from surrealdb import AsyncSurrealDB, SurrealDbConnectionError -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncAuth(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.params = TestConnectionParams() - self.db = AsyncSurrealDB(self.params.url) - - await self.db.connect() - await self.db.use(self.params.namespace, self.params.database) - - async def asyncTearDown(self): - await self.db.close() - - async def test_login_success(self): - outcome = await self.db.sign_in("root", "root") - self.assertNotEqual(None, outcome) - - async def test_login_wrong_password(self): - with self.assertRaises(SurrealDbConnectionError) as context: - await self.db.sign_in("root", "wrong") - - self.assertEqual(True, "There was a problem with authentication" in str(context.exception)) - - async def test_login_wrong_username(self): - with self.assertRaises(SurrealDbConnectionError) as context: - await self.db.sign_in("wrong", "root") - - self.assertEqual(True, "There was a problem with authentication" in str(context.exception)) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/async/test_batch.py b/tests/integration/async/test_batch.py deleted file mode 100644 index dcac1084..00000000 --- a/tests/integration/async/test_batch.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import List -from unittest import TestCase, main - -from surrealdb import SurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams -import asyncio -import websockets - - -class TestBatch(TestCase): - - def setUp(self) -> None: - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - self.queries: List[str] = [] - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - self.db.sign_in("root", "root") - - # self.query = """ - # CREATE |product:1000000| CONTENT { - # name: rand::enum('Cruiser Hoodie', 'Surreal T-Shirt'), - # colours: [rand::string(10), rand::string(10),], - # price: rand::int(20, 60), - # time: { - # created_at: rand::time(1611847404, 1706455404), - # updated_at: rand::time(1651155804, 1716906204) - # } - # }; - # """ - # self.db.query(query=self.query) - - def tearDown(self) -> None: - pass - - def test_batch(self): - print("test_batch") - -if __name__ == '__main__': - main() diff --git a/tests/integration/async/test_create.py b/tests/integration/async/test_create.py deleted file mode 100644 index d681c985..00000000 --- a/tests/integration/async/test_create.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Handles the integration tests for creating and deleting using the create function and QL, and delete in QL. -""" - -from typing import List -from unittest import IsolatedAsyncioTestCase, main - -from surrealdb import AsyncSurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncCreate(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.params = TestConnectionParams() - self.db = AsyncSurrealDB(self.params.url) - - self.queries: List[str] = [] - - await self.db.connect() - await self.db.use(self.params.namespace, self.params.database) - await self.db.sign_in("root", "root") - - async def asyncTearDown(self): - for query in self.queries: - await self.db.query(query) - - await self.db.close() - - async def test_create_ql(self): - self.queries = ["DELETE user;"] - - await self.db.query("CREATE user:tobie SET name = 'Tobie';") - await self.db.query("CREATE user:jaime SET name = 'Jaime';") - - outcome = await self.db.query("SELECT * FROM user;") - self.assertEqual( - [ - {"id": RecordID('user', 'jaime'), "name": "Jaime"}, - {"id": RecordID('user', 'tobie'), "name": "Tobie"}, - ], - outcome[0]['result'], - ) - - async def test_delete_ql(self): - self.queries = ["DELETE user;"] - await self.test_create_ql() - - outcome = await self.db.query("DELETE user;") - self.assertEqual([], outcome[0]['result']) - - outcome = await self.db.query("SELECT * FROM user;") - self.assertEqual([], outcome[0]['result']) - - async def test_create_person_with_tags_ql(self): - self.queries = ["DELETE person;"] - - outcome = await self.db.query( - """ - CREATE person:`失败` CONTENT - { - "user": "me", - "pass": "*æ失败", - "really": True, - "tags": ["python", "documentation"], - }; - """ - ) - self.assertEqual( - [ - { - "id": RecordID.parse("person:⟨失败⟩"), - "pass": "*æ失败", - "really": True, - "tags": ["python", "documentation"], - "user": "me", - } - ], - outcome[0]['result'], - ) - - async def test_create_person_with_tags(self): - self.queries = ["DELETE person;"] - - outcome = await self.db.create( - "person:`失败`", - { - "user": "still me", - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - }, - ) - self.assertEqual( - { - "id": RecordID.parse("person:⟨失败⟩"), - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - "user": "still me", - }, - outcome, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/async/test_live.py b/tests/integration/async/test_live.py deleted file mode 100644 index 19bfad2f..00000000 --- a/tests/integration/async/test_live.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -from typing import List -from unittest import IsolatedAsyncioTestCase - -from surrealdb import AsyncSurrealDB, Table -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncLive(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.params = TestConnectionParams() - self.db = AsyncSurrealDB(self.params.url) - - self.queries: List[str] = [] - - await self.db.connect() - await self.db.use(self.params.namespace, self.params.database) - await self.db.sign_in("root", "root") - - async def asyncTearDown(self): - for query in self.queries: - await self.db.query(query) - await self.db.close() - - async def test_live(self): - if self.params.protocol.lower() == "ws": - self.queries = ["DELETE users;"] - - live_id = await self.db.live(Table("users")) - live_queue = await self.db.live_notifications(live_id) - - await self.db.query("CREATE users;") - - notification_data = await asyncio.wait_for(live_queue.get(), 10) # Set timeout - self.assertEqual(notification_data.get("id"), live_id) - self.assertEqual(notification_data.get("action"), "CREATE") - - diff --git a/tests/integration/async/test_merge.py b/tests/integration/async/test_merge.py deleted file mode 100644 index 8899feee..00000000 --- a/tests/integration/async/test_merge.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Tests the Update operation of the AsyncSurrealDB class with query and merge function. -""" - -from typing import List -from unittest import IsolatedAsyncioTestCase, main - -from surrealdb import AsyncSurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncHttpMerge(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.params = TestConnectionParams() - self.db = AsyncSurrealDB(self.params.url) - - self.queries: List[str] = [] - - await self.db.connect() - await self.db.use(self.params.namespace, self.params.database) - await self.db.sign_in("root", "root") - - async def asyncTearDown(self): - for query in self.queries: - await self.db.query(query) - - await self.db.close() - - async def test_merge_person_with_tags(self): - self.queries = ["DELETE user;"] - - await self.db.query("CREATE user:tobie SET name = 'Tobie';") - await self.db.query("CREATE user:jaime SET name = 'Jaime';") - - _ = await self.db.merge( - "user", - { - "active": True, - }, - ) - - outcome = await self.db.query("SELECT * FROM user;") - self.assertEqual( - [ - {"active": True, "id": RecordID.parse("user:jaime"), "name": "Jaime"}, - {"active": True, "id": RecordID.parse("user:tobie"), "name": "Tobie"}, - ], - outcome[0]['result'] - - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/async/test_query.py b/tests/integration/async/test_query.py deleted file mode 100644 index 5408fe89..00000000 --- a/tests/integration/async/test_query.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Right now this is empty as queries are being used in other test files but feel free to add tests here -""" diff --git a/tests/integration/async/test_set.py b/tests/integration/async/test_set.py deleted file mode 100644 index 25580e52..00000000 --- a/tests/integration/async/test_set.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Tests the Set operation of the AsyncSurrealDB class. -""" - -from typing import List -from unittest import main, IsolatedAsyncioTestCase - -from surrealdb import AsyncSurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncSet(IsolatedAsyncioTestCase): - - async def asyncSetUp(self): - self.params = TestConnectionParams() - self.db = AsyncSurrealDB(self.params.url) - - self.queries: List[str] = [] - - await self.db.connect() - await self.db.use(self.params.namespace, self.params.database) - await self.db.sign_in("root", "root") - - async def asyncTearDown(self): - for query in self.queries: - await self.db.query(query) - await self.db.close() - - async def test_set_ql(self): - self.queries = ["DELETE person;"] - query = "CREATE person:100 SET name = 'Tobie', company = 'SurrealDB', skills = ['Rust', 'Go', 'JavaScript'];" - - outcome = await self.db.query(query) - self.assertEqual( - [ - { - "id": RecordID.parse("person:100"), - "name": "Tobie", - "company": "SurrealDB", - "skills": ["Rust", "Go", "JavaScript"], - } - ], - outcome[0]['result'], - ) - - async def test_set(self): - self.queries = ["DELETE person;"] - query = "CREATE person:100 SET name = $name;" - - _ = await self.db.set( - "name", - { - "name": "Tobie", - "last": "Morgan Hitchcock", - }, - ) - _ = await self.db.query(query) - outcome = await self.db.query("SELECT * FROM person;") - self.assertEqual( - [ - { - "id": RecordID.parse("person:100"), - "name": {"last": "Morgan Hitchcock", "name": "Tobie"}, - } - ], - outcome[0]['result'], - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/async/test_update.py b/tests/integration/async/test_update.py deleted file mode 100644 index 912d09c5..00000000 --- a/tests/integration/async/test_update.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Tests the Update operation of the AsyncSurrealDB class with query and update function. -""" - -from typing import List -from unittest import IsolatedAsyncioTestCase, main - -from surrealdb import AsyncSurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncUpdate(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.params = TestConnectionParams() - self.db = AsyncSurrealDB(self.params.url) - - self.queries: List[str] = [] - - await self.db.connect() - await self.db.use(self.params.namespace, self.params.database) - await self.db.sign_in("root", "root") - - async def asyncTearDown(self): - for query in self.queries: - await self.db.query(query) - await self.db.close() - - async def test_update_ql(self): - self.queries = ["DELETE user;"] - - await self.db.query("CREATE user:tobie SET name = 'Tobie';") - await self.db.query("CREATE user:jaime SET name = 'Jaime';") - outcome = await self.db.query( - "UPDATE user SET lastname = 'Morgan Hitchcock';" - ) - self.assertEqual( - [ - { - "id": RecordID.parse("user:jaime"), - "lastname": "Morgan Hitchcock", - "name": "Jaime", - }, - { - "id": RecordID.parse("user:tobie"), - "lastname": "Morgan Hitchcock", - "name": "Tobie", - }, - ], - outcome[0]['result'], - ) - - async def test_update_person_with_tags(self): - self.queries = ["DELETE person;"] - - _ = await self.db.query( - """ - CREATE person:`失败` CONTENT - { - "user": "me", - "pass": "*æ失败", - "really": True, - "tags": ["python", "documentation"], - }; - """ - ) - - outcome = await self.db.update( - # "person:`失败`", - RecordID.parse("person:失败"), - { - "user": "still me", - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - }, - ) - self.assertEqual( - { - "id": RecordID.parse("person:失败"), - "user": "still me", - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - }, - outcome, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/blocking/test_auth.py b/tests/integration/blocking/test_auth.py deleted file mode 100644 index 94f45b51..00000000 --- a/tests/integration/blocking/test_auth.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Handles the integration tests for logging into the database using blocking operations. -""" - -from unittest import TestCase, main - -from surrealdb import SurrealDB -from surrealdb.errors import SurrealDbError -from tests.integration.connection_params import TestConnectionParams - - -class TestAuth(TestCase): - def setUp(self): - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - - def tearDown(self): - self.db.close() - - def login(self, username: str, password: str) -> None: - self.db.sign_in(username, password) - - def test_login_success(self): - self.login("root", "root") - - def test_login_wrong_password(self): - with self.assertRaises(SurrealDbError) as context: - self.login("root", "wrong") - - self.assertEqual(True, "There was a problem with authentication" in str(context.exception)) - - def test_login_wrong_username(self): - with self.assertRaises(SurrealDbError) as context: - self.login("wrong", "root") - - self.assertEqual(True, "There was a problem with authentication" in str(context.exception)) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/blocking/test_create.py b/tests/integration/blocking/test_create.py deleted file mode 100644 index 6d37e2bd..00000000 --- a/tests/integration/blocking/test_create.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Handles the integration tests for creating and deleting using the create function and QL, and delete in QL. -""" - -from typing import List -from unittest import TestCase, main - -from surrealdb import SurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestCreate(TestCase): - def setUp(self): - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - - self.queries: List[str] = [] - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - self.db.sign_in("root", "root") - - def tearDown(self): - for query in self.queries: - self.db.query(query) - self.db.close() - - def test_create_ql(self): - self.queries = ["DELETE user;"] - self.db.query("CREATE user:tobie SET name = 'Tobie';") - self.db.query("CREATE user:jaime SET name = 'Jaime';") - outcome = self.db.query("SELECT * FROM user;") - self.assertEqual( - [ - {"id": RecordID.parse("user:jaime"), "name": "Jaime"}, - {"id": RecordID.parse("user:tobie"), "name": "Tobie"}, - ], - outcome[0]["result"], - ) - - def test_delete_ql(self): - self.queries = ["DELETE user;"] - self.test_create_ql() - outcome = self.db.query("DELETE user;") - self.assertEqual([], outcome[0]["result"]) - - outcome = self.db.query("SELECT * FROM user;") - self.assertEqual([], outcome[0]["result"]) - - def test_create_person_with_tags_ql(self): - self.queries = ["DELETE person;"] - outcome = self.db.query( - """ - CREATE person:`失败` CONTENT - { - "user": "me", - "pass": "*æ失败", - "really": True, - "tags": ["python", "documentation"], - }; - """ - ) - self.assertEqual( - [ - { - "id": RecordID.parse("person:失败"), - "pass": "*æ失败", - "really": True, - "tags": ["python", "documentation"], - "user": "me", - } - ], - outcome[0]["result"], - ) - - def test_create_person_with_tags(self): - self.queries = ["DELETE person;"] - outcome = self.db.create( - RecordID.parse("person:失败"), - { - "user": "still me", - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - }, - ) - self.assertEqual( - { - "id": RecordID.parse("person:失败"), - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - "user": "still me", - }, - outcome, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/blocking/test_live.py b/tests/integration/blocking/test_live.py deleted file mode 100644 index 4dc40bb2..00000000 --- a/tests/integration/blocking/test_live.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import List -from unittest import TestCase - -from surrealdb import SurrealDB, Table -from surrealdb.asyncio_runtime import AsyncioRuntime -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncLive(TestCase): - def setUp(self): - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - - self.queries: List[str] = [] - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - self.db.sign_in("root", "root") - - def tearDown(self): - for query in self.queries: - self.db.query(query) - self.db.close() - - def test_live(self): - if self.params.protocol.lower() == "ws": - self.queries = ["DELETE users;"] - - live_id = self.db.live(Table("users")) - live_queue = self.db.live_notifications(live_id) - - self.db.query("CREATE users;") - - loop_manager = AsyncioRuntime() - notification_data = loop_manager.loop.run_until_complete(live_queue.get()) - self.assertEqual(notification_data.get("id"), live_id) - self.assertEqual(notification_data.get("action"), "CREATE") - - diff --git a/tests/integration/blocking/test_merge.py b/tests/integration/blocking/test_merge.py deleted file mode 100644 index 5e968a0d..00000000 --- a/tests/integration/blocking/test_merge.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -Tests the Update operation of the SurrealDB class with query and merge function. -""" - -from typing import List -from unittest import TestCase, main - -from surrealdb import SurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestMerge(TestCase): - def setUp(self): - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - - self.queries: List[str] = [] - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - self.db.sign_in("root", "root") - - def tearDown(self): - for query in self.queries: - self.db.query(query) - self.db.close() - - def test_merge_person_with_tags(self): - self.queries = ["DELETE user;"] - - self.db.query("CREATE user:tobie SET name = 'Tobie';") - self.db.query("CREATE user:jaime SET name = 'Jaime';") - - _ = self.db.merge( - "user", - { - "active": True, - }, - ) - - outcome = self.db.query("SELECT * FROM user;") - self.assertEqual( - [ - {"active": True, "id": RecordID.parse("user:jaime"), "name": "Jaime"}, - {"active": True, "id": RecordID.parse("user:tobie"), "name": "Tobie"}, - ], - outcome[0]["result"], - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/blocking/test_query.py b/tests/integration/blocking/test_query.py deleted file mode 100644 index 5408fe89..00000000 --- a/tests/integration/blocking/test_query.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Right now this is empty as queries are being used in other test files but feel free to add tests here -""" diff --git a/tests/integration/blocking/test_set.py b/tests/integration/blocking/test_set.py deleted file mode 100644 index f121ae2c..00000000 --- a/tests/integration/blocking/test_set.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -Tests the Set operation of the AsyncSurrealDB class. -""" - -from typing import List -from unittest import TestCase, main - -from surrealdb import SurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestSet(TestCase): - def setUp(self): - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - - self.queries: List[str] = [] - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - self.db.sign_in("root", "root") - - def tearDown(self): - for query in self.queries: - self.db.query(query) - self.db.close() - - def test_set_ql(self): - self.queries = ["DELETE person;"] - query = "CREATE person:100 SET name = 'Tobie', company = 'SurrealDB', skills = ['Rust', 'Go', 'JavaScript'];" - outcome = self.db.query(query) - self.assertEqual( - [ - { - "id": RecordID.parse("person:100"), - "name": "Tobie", - "company": "SurrealDB", - "skills": ["Rust", "Go", "JavaScript"], - } - ], - outcome[0]["result"], - ) - - def test_set(self): - self.queries = ["DELETE person;"] - query = "CREATE person:100 SET name = $name;" - - self.db.set( - "name", - { - "name": "Tobie", - "last": "Morgan Hitchcock", - }, - ) - _ = self.db.query(query) - outcome = self.db.query("SELECT * FROM person;") - self.assertEqual( - [ - { - "id": RecordID.parse("person:100"), - "name": {"last": "Morgan Hitchcock", "name": "Tobie"}, - } - ], - outcome[0]["result"], - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/blocking/test_update.py b/tests/integration/blocking/test_update.py deleted file mode 100644 index 2b3488b6..00000000 --- a/tests/integration/blocking/test_update.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Tests the Update operation of the AsyncSurrealDB class with query and update function. -""" - -from typing import List -from unittest import TestCase, main - -from surrealdb import SurrealDB, RecordID -from tests.integration.connection_params import TestConnectionParams - - -class TestAsyncHttpUpdate(TestCase): - def setUp(self): - self.params = TestConnectionParams() - self.db = SurrealDB(self.params.url) - - self.queries: List[str] = [] - - self.db.connect() - self.db.use(self.params.namespace, self.params.database) - self.db.sign_in("root", "root") - - def tearDown(self): - for query in self.queries: - self.db.query(query) - self.db.close() - - def test_update_ql(self): - self.queries = ["DELETE user;"] - self.db.query("CREATE user:tobie SET name = 'Tobie';") - self.db.query("CREATE user:jaime SET name = 'Jaime';") - outcome = self.db.query( - "UPDATE user SET lastname = 'Morgan Hitchcock';" - ) - self.assertEqual( - [ - {"id": RecordID.parse("user:jaime"), "lastname": "Morgan Hitchcock", "name": "Jaime"}, - {"id": RecordID.parse("user:tobie"), "lastname": "Morgan Hitchcock", "name": "Tobie"}, - ], - outcome[0]["result"], - ) - - def test_update_person_with_tags(self): - self.queries = ["DELETE person;"] - _ = self.db.query( - """ - CREATE person:`失败` CONTENT - { - "user": "me", - "pass": "*æ失败", - "really": True, - "tags": ["python", "documentation"], - }; - """ - ) - - outcome = self.db.update( - RecordID.parse("person:失败"), - { - "user": "still me", - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - }, - ) - - self.assertEqual( - { - "id": RecordID.parse("person:失败"), - "user": "still me", - "pass": "*æ失败", - "really": False, - "tags": ["python", "test"], - }, - outcome, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/integration/connection_params.py b/tests/integration/connection_params.py deleted file mode 100644 index c99bfc45..00000000 --- a/tests/integration/connection_params.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -This file defines the Parameters to be used in the integration tests based off of the environment variables. -""" - -import os - - -class TestConnectionParams: - """ - The Parameters to be used in the integration tests based off of the environment variables. - - Attributes: - protocol (str): The protocol to be used in the URL. - port (int): The port to be used in the URL. - url (str): The URL to be used in the integration tests. - """ - - @property - def protocol(self) -> str: - return os.environ.get("CONNECTION_PROTOCOL", "ws") - - @property - def port(self) -> int: - return os.environ.get("CONNECTION_PORT", 8000) - - @property - def url(self) -> str: - return f"{self.protocol}://localhost:{self.port}" - - @property - def database(self) -> str: - return os.environ.get("CONNECTION_NS", "test") - - @property - def namespace(self) -> str: - return os.environ.get("CONNECTION_DB", "test") diff --git a/tests/scripts/build_big_data_server.sh b/tests/scripts/build_big_data_server.sh deleted file mode 100644 index bd46d5f0..00000000 --- a/tests/scripts/build_big_data_server.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# navigate to directory -SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" -cd $SCRIPTPATH - -cd .. - -if [ -d "./db_snapshots/big_data_snapshot" ]; then - echo "removing the big data snapshot" - rm -rf ./db_snapshots/big_data_snapshot -fi - -dockpack pull -i maxwellflitton/surrealdb-data -d ./db_snapshots/big_data_snapshot -rm ./db_snapshots/big_data_snapshot/package/LOCK - -docker build -f ./builds/big_data_dockerfile -t surrealdb-big-data . diff --git a/tests/scripts/local_build.py b/tests/scripts/local_build.py deleted file mode 100644 index fb580d5d..00000000 --- a/tests/scripts/local_build.py +++ /dev/null @@ -1,101 +0,0 @@ -import fnmatch -import os -import shutil - - -def delete_directory(dir_path: os.path) -> None: - """ - Checks to see if a directory exists and deletes it if it does. - - :param dir_path: the path to the directory. - """ - if os.path.exists(dir_path): - shutil.rmtree(dir_path) - print(f"Directory '{dir_path}' has been deleted.") - else: - print(f"Directory '{dir_path}' does not exist.") - - -def delete_file(file_path: os.path) -> None: - """ - Checks to see if a file exists and deletes it if it does. - - :param file_path: the path to the file. - """ - if os.path.isfile(file_path): - os.remove(file_path) - print(f"File '{file_path}' has been deleted.") - else: - print(f"File '{file_path}' does not exist.") - - -def find_and_move_rust_surrealdb_file( - start_path: os.path, destination_path: os.path, new_name: str -) -> None: - """ - Finds the rust_surrealdb.so file and moves it to the surrealdb directory. - - :param start_path: the path to start the search from for the built .so rust lib. - :param destination_path: the path to move the rust lib to. - :param new_name: the new name of the rust lib .so file. - """ - for root, dirs, files in os.walk(start_path): - if "lib" in root: - for filename in fnmatch.filter(files, "rust_surrealdb*.so"): - source_file = os.path.join(root, filename) - destination_file = os.path.join(destination_path, new_name) - shutil.move(source_file, destination_file) - return destination_file - return None - - -script_path = os.path.abspath(__file__) -script_directory = os.path.dirname(script_path) - -tests_directory = os.path.join(script_directory, "..") -main_directory = os.path.join(script_directory, "..", "..") -target_directory = os.path.join(main_directory, "target") -egg_info_dir = os.path.join(main_directory, "surrealdb.egg-info") -build_dir = os.path.join(main_directory, "build") - -surrealdb_dir = os.path.join(main_directory, "surrealdb") -embedded_rust_lib_dir = os.path.join(main_directory, "surrealdb", "rust_surrealdb.so") -test_venv_dir = os.path.join(tests_directory, "venv") -source_venv = os.path.join(test_venv_dir, "bin", "activate") - - -def main(): - # delete the old dirs and embedded rust lib if present - print("local build: cleaning up old files") - delete_directory(dir_path=test_venv_dir) - delete_directory(dir_path=build_dir) - delete_directory(dir_path=egg_info_dir) - delete_directory(dir_path=target_directory) - delete_file(file_path=embedded_rust_lib_dir) - print("local build: old files cleaned up") - - # setup venv and build the rust lib - print("local build: setting up venv and building rust lib") - os.system(f"python3 -m venv {test_venv_dir}") - print("local build: venv setup") - print("local build: building rust lib") - os.system(f"source {source_venv} && pip install --no-cache-dir {main_directory}") - print("local build: rust lib built") - - # move the rust lib into the surrealdb directory - print("local build: moving rust lib into surrealdb directory") - find_and_move_rust_surrealdb_file( - start_path=build_dir, - destination_path=surrealdb_dir, - new_name="rust_surrealdb.so", - ) - print("local build: rust lib moved into surrealdb directory") - - # cleanup - delete_directory(dir_path=test_venv_dir) - delete_directory(dir_path=build_dir) - delete_directory(dir_path=egg_info_dir) - - -if __name__ == "__main__": - main() diff --git a/tests/scripts/local_build_ci.py b/tests/scripts/local_build_ci.py deleted file mode 100644 index 136182c6..00000000 --- a/tests/scripts/local_build_ci.py +++ /dev/null @@ -1,85 +0,0 @@ -import fnmatch -import os -import shutil - - -def delete_directory(dir_path: os.path) -> None: - """ - Checks to see if a directory exists and deletes it if it does. - - :param dir_path: the path to the directory. - """ - if os.path.exists(dir_path): - shutil.rmtree(dir_path) - print(f"Directory '{dir_path}' has been deleted.") - else: - print(f"Directory '{dir_path}' does not exist.") - - -def delete_file(file_path: os.path) -> None: - """ - Checks to see if a file exists and deletes it if it does. - - :param file_path: the path to the file. - """ - if os.path.isfile(file_path): - os.remove(file_path) - print(f"File '{file_path}' has been deleted.") - else: - print(f"File '{file_path}' does not exist.") - - -def find_and_move_rust_surrealdb_file( - start_path: os.path, destination_path: os.path, new_name: str -) -> None: - """ - Finds the rust_surrealdb.so file and moves it to the surrealdb directory. - - :param start_path: the path to start the search from for the built .so rust lib. - :param destination_path: the path to move the rust lib to. - :param new_name: the new name of the rust lib .so file. - """ - for root, dirs, files in os.walk(start_path): - if "lib" in root: - for filename in fnmatch.filter(files, "rust_surrealdb*.so"): - source_file = os.path.join(root, filename) - destination_file = os.path.join(destination_path, new_name) - shutil.move(source_file, destination_file) - return destination_file - return None - - -script_path = os.path.abspath(__file__) -script_directory = os.path.dirname(script_path) - -tests_directory = os.path.join(script_directory, "..") -main_directory = os.path.join(script_directory, "..", "..") -target_directory = os.path.join(main_directory, "target") -egg_info_dir = os.path.join(main_directory, "surrealdb.egg-info") -build_dir = os.path.join(main_directory, "build") - -surrealdb_dir = os.path.join(main_directory, "surrealdb") -test_venv_dir = os.path.join(tests_directory, "venv") -source_venv = os.path.join(test_venv_dir, "bin", "activate") - - -def main(): - # delete the old dirs and embedded rust lib if present - print("local build: cleaning up old files") - delete_directory(dir_path=build_dir) - delete_directory(dir_path=egg_info_dir) - delete_directory(dir_path=target_directory) - print("local build: old files cleaned up") - - # setup venv - print("local build: setting up venv") - os.system(f"pip install --no-cache-dir {main_directory}") - print("local build: venv setup done") - - # cleanup - delete_directory(dir_path=build_dir) - delete_directory(dir_path=egg_info_dir) - - -if __name__ == "__main__": - main() diff --git a/tests/scripts/runner.py b/tests/scripts/runner.py deleted file mode 100644 index ef4e5790..00000000 --- a/tests/scripts/runner.py +++ /dev/null @@ -1,105 +0,0 @@ -""" -Testing script that runs all the tests and handles the Docker containers as well as the tests. -""" - -import os -import time -import unittest - -import docker - -TEST_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..") -ROOT_DIR = os.path.join(TEST_DIR, "..") -os.environ["PYTHONPATH"] = ROOT_DIR -DOCKER_CLIENT = docker.from_env() - - -class DbInstance: - """ - Handles the Docker container for the SurrealDB. - - Attributes: - version (str): The version of the SurrealDB to run. - port (int): The port to run the SurrealDB on. - container (docker.models.containers.Container): The Docker container instance. - id (str): The id of the Docker container. - """ - - def __init__(self, version: str, port: int = 8000) -> None: - """ - The constructor for the DbInstance class. - - :param version: The version of the SurrealDB to run. - :param port: The port to run the SurrealDB on. - """ - self.version: str = version - self.port: int = port - self.container = None - self.id = None - - def start(self) -> None: - """ - Starts the Docker container. - - :return: None - """ - self.container = DOCKER_CLIENT.containers.run( - image=f"surrealdb/surrealdb:{self.version}", - command="start", - environment={ - "SURREAL_USER": "root", - "SURREAL_PASS": "root", - "SURREAL_LOG": "trace", - }, - ports={"8000/tcp": self.port}, - detach=True, - ) - self.id = self.container.id - - def stop(self) -> None: - """ - Stops the Docker container. - - :return: None - """ - self.container.stop() - - def remove(self) -> None: - """ - Removes the Docker container. - - :return: None - """ - self.container.remove() - - -def run_tests(port: int, protocol: str) -> None: - os.environ["CONNECTION_PROTOCOL"] = f"{protocol}" - os.environ["CONNECTION_PORT"] = f"{port}" - test_loader = unittest.TestLoader() - test_suite = test_loader.discover(start_dir=TEST_DIR, pattern="test*.py") - test_runner = unittest.TextTestRunner(verbosity=2) - _ = test_runner.run(test_suite) - - -if __name__ == "__main__": - port = 8000 - for version in [ - "v2.0.0", - "v1.2.1", - "v1.2.0", - "v1.0.1", - "v1.1.1", - "v1.1.0", - "v1.0.1", - "1.0.0", - ]: - container = DbInstance(version=version, port=port) - container.start() - time.sleep(0.3) - print(f"Running tests for version {version} on port {container.port}") - run_tests(port=container.port, protocol="http") - run_tests(port=container.port, protocol="ws") - container.stop() - container.remove() - port += 1 \ No newline at end of file diff --git a/tests/unit/cbor_types/test_datetime.py b/tests/unit/cbor_types/test_datetime.py deleted file mode 100644 index ec3f4249..00000000 --- a/tests/unit/cbor_types/test_datetime.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest import TestCase, main - -from surrealdb.data.types.datetime import DateTimeCompact -from surrealdb.data.cbor import encode, decode - - -class TestCBOR(TestCase): - def setUp(self): - pass - - def tearDown(self): - pass - - def test_datetime(self): - compact_date_time_array = [1733994058, 83988472] # December 12, 2024 9:00:58.083 AM - - compact_date_time = DateTimeCompact.parse(compact_date_time_array[0], compact_date_time_array[1]) - encoded = encode(compact_date_time) - decoded = decode(encoded) - - self.assertEqual(decoded.get_date_time(), '2024-12-12T09:00:58.083988Z') - - -if __name__ == '__main__': - main() diff --git a/tests/unit/test_cbor.py b/tests/unit/test_cbor.py deleted file mode 100644 index 45139a96..00000000 --- a/tests/unit/test_cbor.py +++ /dev/null @@ -1,50 +0,0 @@ -import uuid -from unittest import TestCase - -from surrealdb.data import GeometryPoint, GeometryLine, Table, GeometryPolygon, GeometryCollection -from surrealdb.data.cbor import encode, decode - - -class TestCBOR(TestCase): - def setUp(self): - pass - - def tearDown(self): - pass - - def test_geometry(self): - point1 = GeometryPoint(10.00, -3.21) - point2 = GeometryPoint(3.10, 0.1) - point3 = GeometryPoint(4.22, 1.51) - - line1 = GeometryLine(point1, point2, point3, point1) - line2 = GeometryLine(point2, point3) - line3 = GeometryLine(point3, point1) - - polygon1 = GeometryPolygon(line1, line2, line3) - - collection1 = GeometryCollection(point1, line1, polygon1) - - data1 = encode(point1) - data2 = encode(line1) - data3 = encode(polygon1) - data4 = encode(collection1) - - # print("Point: ", data1.hex()) - # print("Line: ", data2.hex()) - # print("Polygon: ", data3.hex()) - # print("Encoded Collection: ", data4.hex()) - # - # print("Decoded Point 1", decode(data1)) - # print("Decoded Line 1", decode(data2)) - # print("Decoded Polygon 1", decode(data3)) - # print("Decoded Collection 1", decode(data4)) - - def test_table(self): - table = Table('testtable') - - def test_uuid(self): - uid = uuid.uuid4() - encoded = encode(uid) - decoded = decode(encoded) - diff --git a/tests/unit/test_clib_connection.py b/tests/unit/test_clib_connection.py deleted file mode 100644 index 10766073..00000000 --- a/tests/unit/test_clib_connection.py +++ /dev/null @@ -1,15 +0,0 @@ -from unittest import IsolatedAsyncioTestCase -from logging import getLogger -from surrealdb.connection_clib import CLibConnection -from surrealdb.data.cbor import encode, decode - - -class TestCLibConnection(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.logger = getLogger(__name__) - - self.clib = CLibConnection(base_url='surrealkv://', logger=self.logger, encoder=encode, decoder=decode) - await self.clib.connect() - - # async def test_send(self): - # await self.clib.send('use', "test_ns", "test_db") diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py deleted file mode 100644 index 9590daab..00000000 --- a/tests/unit/test_connection.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Defines the unit tests for the Connection class. -""" -import logging -import threading -from unittest import IsolatedAsyncioTestCase, main -from unittest.mock import patch, AsyncMock, MagicMock - -from surrealdb.connection import Connection, ResponseType, RequestData -from surrealdb.data.cbor import encode, decode - - -class TestConnection(IsolatedAsyncioTestCase): - - async def asyncSetUp(self): - self.logger = logging.getLogger(__name__) - self.url: str = 'http://localhost:8000' - self.con = Connection(base_url=self.url, logger=self.logger, encoder=encode, decoder=decode, timeout=10) - - async def test___init__(self): - self.assertEqual(self.url, self.con._base_url) - self.assertEqual(self.logger, self.con._logger) - - # assert that the locks are of type threading.Lock - self.assertEqual(type(threading.Lock()), self.con._locks[ResponseType.SEND].__class__) - self.assertEqual(type(threading.Lock()), self.con._locks[ResponseType.NOTIFICATION].__class__) - self.assertEqual(type(threading.Lock()), self.con._locks[ResponseType.ERROR].__class__) - - # assert that the queues are of type dict - self.assertEqual(dict(), self.con._queues[ResponseType.SEND]) - self.assertEqual(dict(), self.con._queues[ResponseType.NOTIFICATION]) - self.assertEqual(dict(), self.con._queues[ResponseType.ERROR]) - - async def test_use(self): - with self.assertRaises(NotImplementedError) as context: - await self.con.use("test", "test") - message = str(context.exception) - self.assertEqual("use method must be implemented", message) - - async def test_connect(self): - with self.assertRaises(NotImplementedError) as context: - await self.con.connect() - message = str(context.exception) - self.assertEqual("connect method must be implemented", message) - - async def test_close(self): - with self.assertRaises(NotImplementedError) as context: - await self.con.close() - message = str(context.exception) - self.assertEqual("close method must be implemented", message) - - async def test__make_request(self): - request_data = RequestData(id="1", method="test", params=()) - with self.assertRaises(NotImplementedError) as context: - await self.con._make_request(request_data) - message = str(context.exception) - self.assertEqual("_make_request method must be implemented", message) - - async def test_set(self): - with self.assertRaises(NotImplementedError) as context: - await self.con.set("test", "test") - message = str(context.exception) - self.assertEqual("set method must be implemented", message) - - async def test_unset(self): - with self.assertRaises(NotImplementedError) as context: - await self.con.unset("test") - message = str(context.exception) - self.assertEqual("unset method must be implemented", message) - - async def test_set_token(self): - self.con.set_token("test") - self.assertEqual("test", self.con._auth_token) - - async def test_create_response_queue(self): - # get a queue when there are now queues in the dictionary - outcome = self.con.create_response_queue(response_type=ResponseType.SEND, queue_id="test") - self.assertEqual(self.con._queues[ResponseType.SEND]["test"], outcome) - self.assertEqual(id(outcome), id(self.con._queues[ResponseType.SEND]["test"])) - - # get a queue when there are queues in the dictionary with the same queue_id - outcome_two = self.con.create_response_queue(response_type=ResponseType.SEND, queue_id="test") - self.assertEqual(outcome, outcome_two) - self.assertEqual(id(outcome), id(outcome_two)) - - # get a queue when there are queues in the dictionary with different queue_id - outcome_three = self.con.create_response_queue(response_type=ResponseType.SEND, queue_id="test_two") - self.assertEqual(self.con._queues[1]["test_two"], outcome_three) - self.assertEqual(id(outcome_three), id(self.con._queues[1]["test_two"])) - - async def test_remove_response_queue(self): - self.con.create_response_queue(response_type=ResponseType.SEND, queue_id="test") - self.con.create_response_queue(response_type=ResponseType.SEND, queue_id="test_two") - self.assertEqual(len(self.con._queues[1].keys()), 2) - - self.con.remove_response_queue(response_type=ResponseType.SEND, queue_id="test") - self.assertEqual(len(self.con._queues[1].keys()), 1) - - self.con.remove_response_queue(response_type=ResponseType.SEND, queue_id="test") - self.assertEqual(len(self.con._queues[1].keys()), 1) - - @patch("surrealdb.connection.request_id") - @patch("surrealdb.connection.Connection._make_request", new_callable=AsyncMock) - async def test_send(self, mock__make_request, mock_request_id): - mock_logger = MagicMock() - response_data = {"result": "test"} - self.con._logger = mock_logger - mock__make_request.return_value = response_data - mock_request_id.return_value = "1" - - request_data = RequestData(id="1", method="test", params=("test",)) - result = await self.con.send("test", "test") - - self.assertEqual(response_data, result) - mock__make_request.assert_called_once_with(request_data) - self.assertEqual(3, mock_logger.debug.call_count) - - -if __name__ == '__main__': - main() diff --git a/tests/unit/test_http_connection.py b/tests/unit/test_http_connection.py deleted file mode 100644 index 260f37fc..00000000 --- a/tests/unit/test_http_connection.py +++ /dev/null @@ -1,54 +0,0 @@ -import logging - -from unittest import IsolatedAsyncioTestCase - -from surrealdb.connection_http import HTTPConnection -from surrealdb.data.cbor import encode, decode - - -class TestHTTPConnection(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - logger = logging.getLogger(__name__) - - self.http_con = HTTPConnection(base_url='http://localhost:8000', logger=logger, encoder=encode, decoder=decode, timeout=10) - await self.http_con.connect() - - async def asyncTearDown(self): - await self.http_con.close() - - async def test_send(self): - await self.http_con.use('test', 'test') - _ = await self.http_con.send('signin', {'user': 'root', 'pass': 'root'}) - - async def test_prepare_query_params(self): - query_params = ("SOME SQL QUERY;", { - "key1": "key1" - }) - await self.http_con.set("key2", "key2") - await self.http_con.set("key3", "key3") - - params = self.http_con._prepare_query_method_params(query_params) - self.assertEqual(query_params[0], params[0]) - self.assertEqual({ - "key1": "key1", - "key2": "key2", - "key3": "key3", - }, params[1]) - - await self.http_con.unset("key3") - - params = self.http_con._prepare_query_method_params(query_params) - self.assertEqual(query_params[0], params[0]) - self.assertEqual({ - "key1": "key1", - "key2": "key2", - }, params[1]) - - await self.http_con.unset("key1") # variable key not part of prev set variables - - params = self.http_con._prepare_query_method_params(query_params) - self.assertEqual(query_params[0], params[0]) - self.assertEqual({ - "key1": "key1", - "key2": "key2", - }, params[1]) diff --git a/tests/unit/test_ws_connection.py b/tests/unit/test_ws_connection.py deleted file mode 100644 index 504a87ce..00000000 --- a/tests/unit/test_ws_connection.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -import logging - -from unittest import IsolatedAsyncioTestCase -from unittest.mock import patch - -from surrealdb.connection_ws import WebsocketConnection -from surrealdb.data.cbor import encode, decode - - -class TestWSConnection(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - logger = logging.getLogger(__name__) - self.ws_con = WebsocketConnection(base_url='ws://localhost:8000', logger=logger, encoder=encode, decoder=decode, timeout=10) - await self.ws_con.connect() - - async def asyncTearDown(self): - await self.ws_con.send("query", "DELETE users;") - await self.ws_con.close() - - async def test_one(self): - await self.ws_con.use("test", "test") - token = await self.ws_con.send('signin', {'user': 'root', 'pass': 'root'}) - await self.ws_con.unset("root") - - async def test_send(self): - await self.ws_con.use("test", "test") - token = await self.ws_con.send('signin', {'user': 'root', 'pass': 'root'}) - self.ws_con.set_token(token) - - live_id = await self.ws_con.send("live", "users") - live_queue = await self.ws_con.live_notifications(live_id) - - await self.ws_con.send("query", "CREATE users;") - - notification_data = await asyncio.wait_for(live_queue.get(), 10) # Set timeout - self.assertEqual(notification_data.get("id"), live_id) - self.assertEqual(notification_data.get("action"), "CREATE") diff --git a/tests/unit/utils_change_url_scheme.py b/tests/unit/utils_change_url_scheme.py deleted file mode 100644 index 28ff5e41..00000000 --- a/tests/unit/utils_change_url_scheme.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest import TestCase, main - -from surrealdb.utils import change_url_scheme - - -class TestChangeUrlScheme(TestCase): - - def test_http_to_https(self): - url = "http://example.com" - new_scheme = "https" - expected = "https://example.com" - self.assertEqual(change_url_scheme(url, new_scheme), expected) - - def test_https_to_http(self): - url = "https://example.com" - new_scheme = "http" - expected = "http://example.com" - self.assertEqual(change_url_scheme(url, new_scheme), expected) - - def test_with_port(self): - url = "http://example.com:8080" - new_scheme = "https" - expected = "https://example.com:8080" - self.assertEqual(change_url_scheme(url, new_scheme), expected) - - def test_with_path(self): - url = "http://example.com/path/to/resource" - new_scheme = "https" - expected = "https://example.com/path/to/resource" - self.assertEqual(change_url_scheme(url, new_scheme), expected) - - def test_with_query(self): - url = "http://example.com/path?query=1" - new_scheme = "https" - expected = "https://example.com/path?query=1" - self.assertEqual(change_url_scheme(url, new_scheme), expected) - - def test_with_fragment(self): - url = "http://example.com/path#section" - new_scheme = "https" - expected = "https://example.com/path#section" - self.assertEqual(change_url_scheme(url, new_scheme), expected) - - -if __name__ == '__main__': - main() - diff --git a/tests/unit/cbor_types/__init__.py b/tests/unit_tests/__init__.py similarity index 100% rename from tests/unit/cbor_types/__init__.py rename to tests/unit_tests/__init__.py diff --git a/tests/integration/async/test_patch.py b/tests/unit_tests/connections/__init__.py similarity index 100% rename from tests/integration/async/test_patch.py rename to tests/unit_tests/connections/__init__.py diff --git a/tests/unit_tests/connections/authenticate/__init__.py b/tests/unit_tests/connections/authenticate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/authenticate/test_async_ws.py b/tests/unit_tests/connections/authenticate/test_async_ws.py new file mode 100644 index 00000000..53707a45 --- /dev/null +++ b/tests/unit_tests/connections/authenticate/test_async_ws.py @@ -0,0 +1,29 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_authenticate(self): + outcome = await self.connection.authenticate(token=self.connection.token) + await self.connection.socket.close() + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/create/__init__.py b/tests/unit_tests/connections/create/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/create/test_async_http.py b/tests/unit_tests/connections/create/test_async_http.py new file mode 100644 index 00000000..ba2deedb --- /dev/null +++ b/tests/unit_tests/connections/create/test_async_http.py @@ -0,0 +1,141 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + + async def test_create_string(self): + outcome = await self.connection.create("user") + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 1 + ) + await self.connection.query("DELETE user;") + + async def test_create_string_with_data(self): + outcome = await self.connection.create("user", self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + + async def test_create_string_with_data_and_id(self): + first_outcome = await self.connection.create("user:tobie", self.data) + self.assertEqual("user", first_outcome["id"].table_name) + self.assertEqual("tobie", first_outcome["id"].id) + self.assertEqual(self.password, first_outcome["password"]) + self.assertEqual(self.username, first_outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual("tobie", outcome[0]["id"].id) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + + async def test_create_record_id(self): + record_id = RecordID("user",1) + outcome = await self.connection.create(record_id) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 1 + ) + + await self.connection.query("DELETE user;") + + async def test_create_record_id_with_data(self): + record_id = RecordID("user", 1) + outcome = await self.connection.create(record_id, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + + async def test_create_table(self): + table = Table("user") + outcome = await self.connection.create(table) + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 1 + ) + + await self.connection.query("DELETE user;") + + async def test_create_table_with_data(self): + table = Table("user") + outcome = await self.connection.create(table, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/create/test_async_ws.py b/tests/unit_tests/connections/create/test_async_ws.py new file mode 100644 index 00000000..6db2b7fe --- /dev/null +++ b/tests/unit_tests/connections/create/test_async_ws.py @@ -0,0 +1,148 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + + async def test_create_string(self): + outcome = await self.connection.create("user") + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 1 + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_create_string_with_data(self): + outcome = await self.connection.create("user", self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_create_string_with_data_and_id(self): + first_outcome = await self.connection.create("user:tobie", self.data) + self.assertEqual("user", first_outcome["id"].table_name) + self.assertEqual("tobie", first_outcome["id"].id) + self.assertEqual(self.password, first_outcome["password"]) + self.assertEqual(self.username, first_outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual("tobie", outcome[0]["id"].id) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_create_record_id(self): + record_id = RecordID("user",1) + outcome = await self.connection.create(record_id) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 1 + ) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_create_record_id_with_data(self): + record_id = RecordID("user", 1) + outcome = await self.connection.create(record_id, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_create_table(self): + table = Table("user") + outcome = await self.connection.create(table) + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 1 + ) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_create_table_with_data(self): + table = Table("user") + outcome = await self.connection.create(table, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/create/test_blocking_http.py b/tests/unit_tests/connections/create/test_blocking_http.py new file mode 100644 index 00000000..25c7766b --- /dev/null +++ b/tests/unit_tests/connections/create/test_blocking_http.py @@ -0,0 +1,117 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = BlockingHttpSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + + def test_create_string(self): + outcome = self.connection.create("user") + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(len(self.connection.query("SELECT * FROM user;")), 1) + self.connection.query("DELETE user;") + + def test_create_string_with_data(self): + outcome = self.connection.create("user", self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(len(outcome), 1) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + self.connection.query("DELETE user;") + + def test_create_string_with_data_and_id(self): + first_outcome = self.connection.create("user:tobie", self.data) + self.assertEqual("user", first_outcome["id"].table_name) + self.assertEqual("tobie", first_outcome["id"].id) + self.assertEqual(self.password, first_outcome["password"]) + self.assertEqual(self.username, first_outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(len(outcome), 1) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual("tobie", outcome[0]["id"].id) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + self.connection.query("DELETE user;") + + def test_create_record_id(self): + record_id = RecordID("user", 1) + outcome = self.connection.create(record_id) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + + self.assertEqual(len(self.connection.query("SELECT * FROM user;")), 1) + + self.connection.query("DELETE user;") + + def test_create_record_id_with_data(self): + record_id = RecordID("user", 1) + outcome = self.connection.create(record_id, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(len(outcome), 1) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + self.connection.query("DELETE user;") + + def test_create_table(self): + table = Table("user") + outcome = self.connection.create(table) + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual(len(self.connection.query("SELECT * FROM user;")), 1) + + self.connection.query("DELETE user;") + + def test_create_table_with_data(self): + table = Table("user") + outcome = self.connection.create(table, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(len(outcome), 1) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/create/test_blocking_ws.py b/tests/unit_tests/connections/create/test_blocking_ws.py new file mode 100644 index 00000000..dd69727e --- /dev/null +++ b/tests/unit_tests/connections/create/test_blocking_ws.py @@ -0,0 +1,130 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingWsSurrealConnection(TestCase): + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def test_create_string(self): + outcome = self.connection.create("user") + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual( + len(self.connection.query("SELECT * FROM user;")), + 1 + ) + + def test_create_string_with_data(self): + outcome = self.connection.create("user", self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + def test_create_string_with_data_and_id(self): + first_outcome = self.connection.create("user:tobie", self.data) + self.assertEqual("user", first_outcome["id"].table_name) + self.assertEqual("tobie", first_outcome["id"].id) + self.assertEqual(self.password, first_outcome["password"]) + self.assertEqual(self.username, first_outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual("tobie", outcome[0]["id"].id) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + def test_create_record_id(self): + record_id = RecordID("user", 1) + outcome = self.connection.create(record_id) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + + self.assertEqual( + len(self.connection.query("SELECT * FROM user;")), + 1 + ) + + def test_create_record_id_with_data(self): + record_id = RecordID("user", 1) + outcome = self.connection.create(record_id, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(1, outcome["id"].id) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + def test_create_table(self): + table = Table("user") + outcome = self.connection.create(table) + self.assertEqual("user", outcome["id"].table_name) + + self.assertEqual( + len(self.connection.query("SELECT * FROM user;")), + 1 + ) + + def test_create_table_with_data(self): + table = Table("user") + outcome = self.connection.create(table, self.data) + self.assertEqual("user", outcome["id"].table_name) + self.assertEqual(self.password, outcome["password"]) + self.assertEqual(self.username, outcome["username"]) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual( + len(outcome), + 1 + ) + self.assertEqual("user", outcome[0]["id"].table_name) + self.assertEqual(self.password, outcome[0]["password"]) + self.assertEqual(self.username, outcome[0]["username"]) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/delete/__init__.py b/tests/unit_tests/connections/delete/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/delete/test_async_http.py b/tests/unit_tests/connections/delete/test_async_http.py new file mode 100644 index 00000000..cbba2adc --- /dev/null +++ b/tests/unit_tests/connections/delete/test_async_http.py @@ -0,0 +1,63 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_delete_string(self): + outcome = await self.connection.delete("user:tobie") + self.check_no_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + async def test_delete_record_id(self): + first_outcome = await self.connection.delete(self.record_id) + self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + async def test_delete_table(self): + await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + table = Table("user") + first_outcome = await self.connection.delete(table) + self.assertEqual(2, len(first_outcome)) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/delete/test_async_ws.py b/tests/unit_tests/connections/delete/test_async_ws.py new file mode 100644 index 00000000..4a6f73f7 --- /dev/null +++ b/tests/unit_tests/connections/delete/test_async_ws.py @@ -0,0 +1,66 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_delete_string(self): + outcome = await self.connection.delete("user:tobie") + self.check_no_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + await self.connection.socket.close() + + async def test_delete_record_id(self): + first_outcome = await self.connection.delete(self.record_id) + self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + await self.connection.socket.close() + + async def test_delete_table(self): + await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + table = Table("user") + first_outcome = await self.connection.delete(table) + self.assertEqual(2, len(first_outcome)) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/delete/test_blocking_http.py b/tests/unit_tests/connections/delete/test_blocking_http.py new file mode 100644 index 00000000..8122c6d5 --- /dev/null +++ b/tests/unit_tests/connections/delete/test_blocking_http.py @@ -0,0 +1,62 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingHttpSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_delete_string(self): + outcome = self.connection.delete("user:tobie") + self.check_no_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + def test_delete_record_id(self): + first_outcome = self.connection.delete(self.record_id) + self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + def test_delete_table(self): + self.connection.query("CREATE user:jaime SET name = 'Jaime';") + table = Table("user") + first_outcome = self.connection.delete(table) + self.assertEqual(2, len(first_outcome)) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/delete/test_blocking_ws.py b/tests/unit_tests/connections/delete/test_blocking_ws.py new file mode 100644 index 00000000..85ab3465 --- /dev/null +++ b/tests/unit_tests/connections/delete/test_blocking_ws.py @@ -0,0 +1,71 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_delete_string(self): + outcome = self.connection.delete("user:tobie") + self.check_no_change(outcome) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + def test_delete_record_id(self): + first_outcome = self.connection.delete(self.record_id) + self.check_no_change(first_outcome) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + def test_delete_table(self): + self.connection.query("CREATE user:jaime SET name = 'Jaime';") + table = Table("user") + + first_outcome = self.connection.delete(table) + self.assertEqual(2, len(first_outcome)) + + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(outcome, []) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/info/__init__.py b/tests/unit_tests/connections/info/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/info/test_async_http.py b/tests/unit_tests/connections/info/test_async_http.py new file mode 100644 index 00000000..3dd89222 --- /dev/null +++ b/tests/unit_tests/connections/info/test_async_http.py @@ -0,0 +1,29 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_info(self): + outcome = await self.connection.info() + # TODO => confirm that the info is what we expect + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/info/test_async_ws.py b/tests/unit_tests/connections/info/test_async_ws.py new file mode 100644 index 00000000..7c77fe1e --- /dev/null +++ b/tests/unit_tests/connections/info/test_async_ws.py @@ -0,0 +1,30 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_info(self): + outcome = await self.connection.info() + await self.connection.socket.close() + # TODO => confirm that the info is what we expect + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/info/test_block_ws.py b/tests/unit_tests/connections/info/test_block_ws.py new file mode 100644 index 00000000..f07f1ac9 --- /dev/null +++ b/tests/unit_tests/connections/info/test_block_ws.py @@ -0,0 +1,32 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + + def tearDown(self): + if self.connection.socket: + self.connection.socket.close() + + def test_info(self): + outcome = self.connection.info() + # TODO => confirm that the info is what we expect + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/info/test_blocking_http.py b/tests/unit_tests/connections/info/test_blocking_http.py new file mode 100644 index 00000000..f53803c8 --- /dev/null +++ b/tests/unit_tests/connections/info/test_blocking_http.py @@ -0,0 +1,29 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + + def test_info(self): + outcome = self.connection.info() + # TODO => confirm that the info is what we expect + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert/__init__.py b/tests/unit_tests/connections/insert/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/insert/test_async_http.py b/tests/unit_tests/connections/insert/test_async_http.py new file mode 100644 index 00000000..030df438 --- /dev/null +++ b/tests/unit_tests/connections/insert/test_async_http.py @@ -0,0 +1,63 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.insert_bulk_data = [ + { + "name": "Tobie", + }, + { + "name": "Jaime" + } + ] + self.insert_data = [ + { + "name": "Tobie", + } + ] + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + + async def test_insert_string_with_data(self): + outcome = await self.connection.insert("user", self.insert_bulk_data) + self.assertEqual(2, len(outcome)) + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 2 + ) + await self.connection.query("DELETE user;") + + async def test_insert_record_id_result_error(self): + record_id = RecordID("user","tobie") + + with self.assertRaises(Exception) as context: + _ = await self.connection.insert(record_id, self.insert_data) + self.assertEqual( + "There was a problem with the database: Can not execute INSERT statement using value 'user:tobie'" in str(context.exception), + True + ) + await self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert/test_async_ws.py b/tests/unit_tests/connections/insert/test_async_ws.py new file mode 100644 index 00000000..0f9e69b2 --- /dev/null +++ b/tests/unit_tests/connections/insert/test_async_ws.py @@ -0,0 +1,65 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.insert_bulk_data = [ + { + "name": "Tobie", + }, + { + "name": "Jaime" + } + ] + self.insert_data = [ + { + "name": "Tobie", + } + ] + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + + async def test_insert_string_with_data(self): + outcome = await self.connection.insert("user", self.insert_bulk_data) + self.assertEqual(2, len(outcome)) + self.assertEqual( + len(await self.connection.query("SELECT * FROM user;")), + 2 + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_insert_record_id_result_error(self): + record_id = RecordID("user","tobie") + + with self.assertRaises(Exception) as context: + _ = await self.connection.insert(record_id, self.insert_data) + self.assertEqual( + "There was a problem with the database: Can not execute INSERT statement using value 'user:tobie'" in str(context.exception), + True + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert/test_blocking_http.py b/tests/unit_tests/connections/insert/test_blocking_http.py new file mode 100644 index 00000000..23e58d47 --- /dev/null +++ b/tests/unit_tests/connections/insert/test_blocking_http.py @@ -0,0 +1,67 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.insert_bulk_data = [ + { + "name": "Tobie", + }, + { + "name": "Jaime" + } + ] + self.insert_data = [ + { + "name": "Tobie", + } + ] + self.connection = BlockingHttpSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + + def test_info(self): + outcome = self.connection.info() + # TODO => confirm that the info is what we expect + + def test_insert_string_with_data(self): + outcome = self.connection.insert("user", self.insert_bulk_data) + self.assertEqual(2, len(outcome)) + self.assertEqual( + len(self.connection.query("SELECT * FROM user;")), + 2 + ) + self.connection.query("DELETE user;") + + def test_insert_record_id_result_error(self): + record_id = RecordID("user","tobie") + + with self.assertRaises(Exception) as context: + _ = self.connection.insert(record_id, self.insert_data) + self.assertTrue( + "There was a problem with the database: Can not execute INSERT statement using value 'user:tobie'" in str(context.exception) + ) + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert/test_blocking_ws.py b/tests/unit_tests/connections/insert/test_blocking_ws.py new file mode 100644 index 00000000..02066946 --- /dev/null +++ b/tests/unit_tests/connections/insert/test_blocking_ws.py @@ -0,0 +1,66 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.insert_bulk_data = [ + { + "name": "Tobie", + }, + { + "name": "Jaime" + } + ] + self.insert_data = [ + { + "name": "Tobie", + } + ] + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def test_insert_string_with_data(self): + outcome = self.connection.insert("user", self.insert_bulk_data) + self.assertEqual(2, len(outcome)) + self.assertEqual( + len(self.connection.query("SELECT * FROM user;")), + 2 + ) + + def test_insert_record_id_result_error(self): + record_id = RecordID("user", "tobie") + + with self.assertRaises(Exception) as context: + _ = self.connection.insert(record_id, self.insert_data) + self.assertTrue( + "There was a problem with the database: Can not execute INSERT statement using value 'user:tobie'" + in str(context.exception) + ) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert_relation/__init__.py b/tests/unit_tests/connections/insert_relation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/insert_relation/test_async_http.py b/tests/unit_tests/connections/insert_relation/test_async_http.py new file mode 100644 index 00000000..0301a805 --- /dev/null +++ b/tests/unit_tests/connections/insert_relation/test_async_http.py @@ -0,0 +1,99 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("DELETE likes;") + await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_outcome(self, outcome: list): + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + + async def test_insert_relation_record_ids(self): + data = [ + { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + }, + { + "in": RecordID('user', 'jaime'), + "out": RecordID('likes', 400), + }, + ] + outcome = await self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + await self.connection.query("DELETE user;") + await self.connection.query("DELETE likes;") + + async def test_insert_relation_record_id(self): + data = { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + } + outcome = await self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + await self.connection.query("DELETE user;") + await self.connection.query("DELETE likes;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert_relation/test_async_ws.py b/tests/unit_tests/connections/insert_relation/test_async_ws.py new file mode 100644 index 00000000..9ae31560 --- /dev/null +++ b/tests/unit_tests/connections/insert_relation/test_async_ws.py @@ -0,0 +1,101 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("DELETE likes;") + await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_outcome(self, outcome: list): + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + + async def test_insert_relation_record_ids(self): + data = [ + { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + }, + { + "in": RecordID('user', 'jaime'), + "out": RecordID('likes', 400), + }, + ] + outcome = await self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + await self.connection.query("DELETE user;") + await self.connection.query("DELETE likes;") + await self.connection.socket.close() + + async def test_insert_relation_record_id(self): + data = { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + } + outcome = await self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + await self.connection.query("DELETE user;") + await self.connection.query("DELETE likes;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert_relation/test_blocking_http.py b/tests/unit_tests/connections/insert_relation/test_blocking_http.py new file mode 100644 index 00000000..216f30c5 --- /dev/null +++ b/tests/unit_tests/connections/insert_relation/test_blocking_http.py @@ -0,0 +1,99 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestblockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("DELETE likes;") + self.connection.query("CREATE user:jaime SET name = 'Jaime';") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_outcome(self, outcome: list): + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + + def test_insert_relation_record_ids(self): + data = [ + { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + }, + { + "in": RecordID('user', 'jaime'), + "out": RecordID('likes', 400), + }, + ] + outcome = self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + self.connection.query("DELETE user;") + self.connection.query("DELETE likes;") + + def test_insert_relation_record_id(self): + data = { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + } + outcome = self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.connection.query("DELETE user;") + self.connection.query("DELETE likes;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/insert_relation/test_blocking_ws.py b/tests/unit_tests/connections/insert_relation/test_blocking_ws.py new file mode 100644 index 00000000..6a947054 --- /dev/null +++ b/tests/unit_tests/connections/insert_relation/test_blocking_ws.py @@ -0,0 +1,101 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "username": self.username, + "password": self.password, + } + self.connection = BlockingWsSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("DELETE likes;") + self.connection.query("CREATE user:jaime SET name = 'Jaime';") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_outcome(self, outcome: list): + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + + def test_insert_relation_record_ids(self): + data = [ + { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + }, + { + "in": RecordID('user', 'jaime'), + "out": RecordID('likes', 400), + }, + ] + outcome = self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.assertEqual( + RecordID('user', 'jaime'), + outcome[1]["in"] + ) + self.assertEqual( + RecordID('likes', 400), + outcome[1]["out"] + ) + self.connection.query("DELETE user;") + self.connection.query("DELETE likes;") + self.connection.socket.close() + + def test_insert_relation_record_id(self): + data = { + "in": RecordID('user', 'tobie'), + "out": RecordID('likes', 123), + } + outcome = self.connection.insert_relation("likes", data) + self.assertEqual( + RecordID('user', 'tobie'), + outcome[0]["in"] + ) + self.assertEqual( + RecordID('likes', 123), + outcome[0]["out"] + ) + self.connection.query("DELETE user;") + self.connection.query("DELETE likes;") + self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/invalidate/__init__.py b/tests/unit_tests/connections/invalidate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/invalidate/test_async_http.py b/tests/unit_tests/connections/invalidate/test_async_http.py new file mode 100644 index 00000000..66c1991b --- /dev/null +++ b/tests/unit_tests/connections/invalidate/test_async_http.py @@ -0,0 +1,33 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_invalidate(self): + _ = await self.connection.invalidate() + with self.assertRaises(Exception) as context: + _ = await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + self.assertEqual( + "IAM error: Not enough permissions" in str(context.exception), + True + ) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/invalidate/test_async_ws.py b/tests/unit_tests/connections/invalidate/test_async_ws.py new file mode 100644 index 00000000..36413657 --- /dev/null +++ b/tests/unit_tests/connections/invalidate/test_async_ws.py @@ -0,0 +1,35 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_invalidate(self): + _ = await self.connection.invalidate() + with self.assertRaises(Exception) as context: + _ = await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + self.assertEqual( + "IAM error: Not enough permissions" in str(context.exception), + True + ) + await self.connection.socket.close() + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/invalidate/test_blocking_http.py b/tests/unit_tests/connections/invalidate/test_blocking_http.py new file mode 100644 index 00000000..14c8ecc7 --- /dev/null +++ b/tests/unit_tests/connections/invalidate/test_blocking_http.py @@ -0,0 +1,33 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + + def test_invalidate(self): + _ = self.connection.invalidate() + with self.assertRaises(Exception) as context: + _ = self.connection.query("CREATE user:jaime SET name = 'Jaime';") + self.assertEqual( + "IAM error: Not enough permissions" in str(context.exception), + True + ) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/invalidate/test_blocking_ws.py b/tests/unit_tests/connections/invalidate/test_blocking_ws.py new file mode 100644 index 00000000..36413657 --- /dev/null +++ b/tests/unit_tests/connections/invalidate/test_blocking_ws.py @@ -0,0 +1,35 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_invalidate(self): + _ = await self.connection.invalidate() + with self.assertRaises(Exception) as context: + _ = await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + self.assertEqual( + "IAM error: Not enough permissions" in str(context.exception), + True + ) + await self.connection.socket.close() + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/let/__init__.py b/tests/unit_tests/connections/let/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/let/test_async_http.py b/tests/unit_tests/connections/let/test_async_http.py new file mode 100644 index 00000000..e39edd61 --- /dev/null +++ b/tests/unit_tests/connections/let/test_async_http.py @@ -0,0 +1,36 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE person;") + + async def test_let(self): + outcome = await self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertEqual(None, outcome) + await self.connection.query('CREATE person SET name = $name') + outcome = await self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + await self.connection.query("DELETE person;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/let/test_async_ws.py b/tests/unit_tests/connections/let/test_async_ws.py new file mode 100644 index 00000000..a4faeda5 --- /dev/null +++ b/tests/unit_tests/connections/let/test_async_ws.py @@ -0,0 +1,37 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE person;") + + async def test_let(self): + outcome = await self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertEqual(None, outcome) + await self.connection.query('CREATE person SET name = $name') + outcome = await self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + await self.connection.query("DELETE person;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/let/test_blocking_http.py b/tests/unit_tests/connections/let/test_blocking_http.py new file mode 100644 index 00000000..131b8350 --- /dev/null +++ b/tests/unit_tests/connections/let/test_blocking_http.py @@ -0,0 +1,36 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE person;") + + def test_let(self): + outcome = self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertEqual(None, outcome) + self.connection.query('CREATE person SET name = $name') + outcome = self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + self.connection.query("DELETE person;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/let/test_blocking_ws.py b/tests/unit_tests/connections/let/test_blocking_ws.py new file mode 100644 index 00000000..492a8e6e --- /dev/null +++ b/tests/unit_tests/connections/let/test_blocking_ws.py @@ -0,0 +1,41 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE person;") + + def tearDown(self): + self.connection.query("DELETE person;") + if self.connection.socket: + self.connection.socket.close() + + def test_let(self): + outcome = self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertIsNone(outcome) + + self.connection.query('CREATE person SET name = $name') + outcome = self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/live/__init__.py b/tests/unit_tests/connections/live/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/live/test_async_http.py b/tests/unit_tests/connections/live/test_async_http.py new file mode 100644 index 00000000..80b054e7 --- /dev/null +++ b/tests/unit_tests/connections/live/test_async_http.py @@ -0,0 +1,33 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from uuid import UUID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + async def test_query(self): + outcome = await self.connection.live("user") + self.assertEqual(UUID, type(outcome)) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/live/test_async_ws.py b/tests/unit_tests/connections/live/test_async_ws.py new file mode 100644 index 00000000..80b054e7 --- /dev/null +++ b/tests/unit_tests/connections/live/test_async_ws.py @@ -0,0 +1,33 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from uuid import UUID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + async def test_query(self): + outcome = await self.connection.live("user") + self.assertEqual(UUID, type(outcome)) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/live/test_blocking_ws.py b/tests/unit_tests/connections/live/test_blocking_ws.py new file mode 100644 index 00000000..dc492ea9 --- /dev/null +++ b/tests/unit_tests/connections/live/test_blocking_ws.py @@ -0,0 +1,36 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from uuid import UUID + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def test_query(self): + outcome = self.connection.live("user") + self.assertEqual(UUID, type(outcome)) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/merge/__init__.py b/tests/unit_tests/connections/merge/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/merge/test_async_http.py b/tests/unit_tests/connections/merge/test_async_http.py new file mode 100644 index 00000000..2524eaf6 --- /dev/null +++ b/tests/unit_tests/connections/merge/test_async_http.py @@ -0,0 +1,103 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_merge_string(self): + outcome = await self.connection.merge("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + + + async def test_merge_string_with_data(self): + first_outcome = await self.connection.merge("user:tobie", self.data) + self.check_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + + async def test_merge_record_id(self): + first_outcome = await self.connection.merge(self.record_id) + self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + + + async def test_merge_record_id_with_data(self): + outcome = await self.connection.merge(self.record_id, self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change( + outcome[0] + ) + await self.connection.query("DELETE user;") + + + async def test_merge_table(self): + table = Table("user") + first_outcome = await self.connection.merge(table) + self.check_no_change(first_outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + await self.connection.query("DELETE user;") + + + async def test_merge_table_with_data(self): + table = Table("user") + outcome = await self.connection.merge(table, self.data) + self.check_change(outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/merge/test_async_ws.py b/tests/unit_tests/connections/merge/test_async_ws.py new file mode 100644 index 00000000..1f151842 --- /dev/null +++ b/tests/unit_tests/connections/merge/test_async_ws.py @@ -0,0 +1,103 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_merge_string(self): + outcome = await self.connection.merge("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_merge_string_with_data(self): + first_outcome = await self.connection.merge("user:tobie", self.data) + self.check_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_merge_record_id(self): + first_outcome = await self.connection.merge(self.record_id) + self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_merge_record_id_with_data(self): + outcome = await self.connection.merge(self.record_id, self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change( + outcome[0] + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_merge_table(self): + table = Table("user") + first_outcome = await self.connection.merge(table) + self.check_no_change(first_outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_merge_table_with_data(self): + table = Table("user") + outcome = await self.connection.merge(table, self.data) + self.check_change(outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/merge/test_blocking_http.py b/tests/unit_tests/connections/merge/test_blocking_http.py new file mode 100644 index 00000000..a61e6d2f --- /dev/null +++ b/tests/unit_tests/connections/merge/test_blocking_http.py @@ -0,0 +1,103 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_merge_string(self): + outcome = self.connection.merge("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + self.connection.query("DELETE user;") + + + def test_merge_string_with_data(self): + first_outcome = self.connection.merge("user:tobie", self.data) + self.check_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + + def test_merge_record_id(self): + first_outcome = self.connection.merge(self.record_id) + self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + self.connection.query("DELETE user;") + + + def test_merge_record_id_with_data(self): + outcome = self.connection.merge(self.record_id, self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change( + outcome[0] + ) + self.connection.query("DELETE user;") + + + def test_merge_table(self): + table = Table("user") + first_outcome = self.connection.merge(table) + self.check_no_change(first_outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + self.connection.query("DELETE user;") + + + def test_merge_table_with_data(self): + table = Table("user") + outcome = self.connection.merge(table, self.data) + self.check_change(outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/merge/test_blocking_ws.py b/tests/unit_tests/connections/merge/test_blocking_ws.py new file mode 100644 index 00000000..7984ab82 --- /dev/null +++ b/tests/unit_tests/connections/merge/test_blocking_ws.py @@ -0,0 +1,92 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual("Tobie", data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual("Jaime", data["name"]) + self.assertEqual(35, data["age"]) + + def test_merge_string(self): + outcome = self.connection.merge("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + def test_merge_string_with_data(self): + first_outcome = self.connection.merge("user:tobie", self.data) + self.check_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + def test_merge_record_id(self): + first_outcome = self.connection.merge(self.record_id) + self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + def test_merge_record_id_with_data(self): + outcome = self.connection.merge(self.record_id, self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + def test_merge_table(self): + table = Table("user") + first_outcome = self.connection.merge(table) + self.check_no_change(first_outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + def test_merge_table_with_data(self): + table = Table("user") + outcome = self.connection.merge(table, self.data) + self.check_change(outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/patch/__init__.py b/tests/unit_tests/connections/patch/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/patch/test_async_http.py b/tests/unit_tests/connections/patch/test_async_http.py new file mode 100644 index 00000000..e6454e0d --- /dev/null +++ b/tests/unit_tests/connections/patch/test_async_http.py @@ -0,0 +1,64 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.record_id = RecordID(table_name="user", identifier="tobie") + self.data = [ + { "op": "replace", "path": "/name", "value": "Jaime" }, + { "op": "replace", "path": "/age", "value": 35 }, + ] + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_patch_string_with_data(self): + outcome = await self.connection.patch("user:tobie", self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_patch_record_id_with_data(self): + outcome = await self.connection.patch(self.record_id, self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_patch_table_with_data(self): + table = Table("user") + outcome = await self.connection.patch(table, self.data) + self.check_change(outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/patch/test_async_ws.py b/tests/unit_tests/connections/patch/test_async_ws.py new file mode 100644 index 00000000..f4c54327 --- /dev/null +++ b/tests/unit_tests/connections/patch/test_async_ws.py @@ -0,0 +1,67 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.record_id = RecordID(table_name="user", identifier="tobie") + self.data = [ + { "op": "replace", "path": "/name", "value": "Jaime" }, + { "op": "replace", "path": "/age", "value": 35 }, + ] + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_patch_string_with_data(self): + outcome = await self.connection.patch("user:tobie", self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_patch_record_id_with_data(self): + outcome = await self.connection.patch(self.record_id, self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_patch_table_with_data(self): + table = Table("user") + outcome = await self.connection.patch(table, self.data) + self.check_change(outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/patch/test_blocking_http.py b/tests/unit_tests/connections/patch/test_blocking_http.py new file mode 100644 index 00000000..5782f6f8 --- /dev/null +++ b/tests/unit_tests/connections/patch/test_blocking_http.py @@ -0,0 +1,64 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.record_id = RecordID(table_name="user", identifier="tobie") + self.data = [ + { "op": "replace", "path": "/name", "value": "Jaime" }, + { "op": "replace", "path": "/age", "value": 35 }, + ] + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_patch_string_with_data(self): + outcome = self.connection.patch("user:tobie", self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_patch_record_id_with_data(self): + outcome = self.connection.patch(self.record_id, self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_patch_table_with_data(self): + table = Table("user") + outcome = self.connection.patch(table, self.data) + self.check_change(outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/patch/test_blocking_ws.py b/tests/unit_tests/connections/patch/test_blocking_ws.py new file mode 100644 index 00000000..b79a7fa4 --- /dev/null +++ b/tests/unit_tests/connections/patch/test_blocking_ws.py @@ -0,0 +1,66 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.record_id = RecordID(table_name="user", identifier="tobie") + self.data = [ + {"op": "replace", "path": "/name", "value": "Jaime"}, + {"op": "replace", "path": "/age", "value": 35}, + ] + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual("Tobie", data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual("Jaime", data["name"]) + self.assertEqual(35, data["age"]) + + def test_patch_string_with_data(self): + outcome = self.connection.patch("user:tobie", self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + def test_patch_record_id_with_data(self): + outcome = self.connection.patch(self.record_id, self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + def test_patch_table_with_data(self): + table = Table("user") + outcome = self.connection.patch(table, self.data) + self.check_change(outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/query/__init__.py b/tests/unit_tests/connections/query/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/query/test_async_http.py b/tests/unit_tests/connections/query/test_async_http.py new file mode 100644 index 00000000..444f6fb1 --- /dev/null +++ b/tests/unit_tests/connections/query/test_async_http.py @@ -0,0 +1,62 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.queries = ["DELETE user;"] + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_query(self): + await self.connection.query("DELETE user;") + self.assertEqual( + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + [ + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie" + } + ] + ) + self.assertEqual( + await self.connection.query("CREATE user:jaime SET name = 'Jaime';"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime" + } + ] + ) + self.assertEqual( + await self.connection.query("SELECT * FROM user;"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime" + }, + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie" + } + ] + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/query/test_async_ws.py b/tests/unit_tests/connections/query/test_async_ws.py new file mode 100644 index 00000000..7ffcf483 --- /dev/null +++ b/tests/unit_tests/connections/query/test_async_ws.py @@ -0,0 +1,62 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.queries = ["DELETE user;"] + self.url = "ws://localhost:8000/rpc" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_query(self): + await self.connection.query("DELETE user;") + self.assertEqual( + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + [ + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie" + } + ] + ) + self.assertEqual( + await self.connection.query("CREATE user:jaime SET name = 'Jaime';"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime" + } + ] + ) + self.assertEqual( + await self.connection.query("SELECT * FROM user;"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime" + }, + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie" + } + ] + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/query/test_blocking_http.py b/tests/unit_tests/connections/query/test_blocking_http.py new file mode 100644 index 00000000..2877d896 --- /dev/null +++ b/tests/unit_tests/connections/query/test_blocking_http.py @@ -0,0 +1,61 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestBlockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.queries = ["DELETE user;"] + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + + def test_query(self): + self.connection.query("DELETE user;") + self.assertEqual( + self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + [ + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie" + } + ] + ) + self.assertEqual( + self.connection.query("CREATE user:jaime SET name = 'Jaime';"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime" + } + ] + ) + self.assertEqual( + self.connection.query("SELECT * FROM user;"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime" + }, + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie" + } + ] + ) + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/query/test_blocking_ws.py b/tests/unit_tests/connections/query/test_blocking_ws.py new file mode 100644 index 00000000..3cf050a3 --- /dev/null +++ b/tests/unit_tests/connections/query/test_blocking_ws.py @@ -0,0 +1,66 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.queries = ["DELETE user;"] + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def test_query(self): + self.connection.query("DELETE user;") + self.assertEqual( + self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + [ + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie", + } + ], + ) + self.assertEqual( + self.connection.query("CREATE user:jaime SET name = 'Jaime';"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime", + } + ], + ) + self.assertEqual( + self.connection.query("SELECT * FROM user;"), + [ + { + "id": RecordID(table_name="user", identifier="jaime"), + "name": "Jaime", + }, + { + "id": RecordID(table_name="user", identifier="tobie"), + "name": "Tobie", + }, + ], + ) + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/select/__init__.py b/tests/unit_tests/connections/select/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/select/test_async_http.py b/tests/unit_tests/connections/select/test_async_http.py new file mode 100644 index 00000000..087e749b --- /dev/null +++ b/tests/unit_tests/connections/select/test_async_http.py @@ -0,0 +1,48 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_select(self): + await self.connection.query("DELETE user;") + await self.connection.query("DELETE users;") + + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + + await self.connection.query("CREATE users:one SET name = 'one';") + await self.connection.query("CREATE users:two SET name = 'two';") + + outcome = await self.connection.select("user") + self.assertEqual( + outcome[0]["name"], + "Jaime", + ) + self.assertEqual( + outcome[1]["name"], + "Tobie", + ) + self.assertEqual(2, len(outcome)) + + await self.connection.query("DELETE user;") + await self.connection.query("DELETE users;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/select/test_async_ws.py b/tests/unit_tests/connections/select/test_async_ws.py new file mode 100644 index 00000000..71328260 --- /dev/null +++ b/tests/unit_tests/connections/select/test_async_ws.py @@ -0,0 +1,49 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_select(self): + await self.connection.query("DELETE user;") + await self.connection.query("DELETE users;") + + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + await self.connection.query("CREATE user:jaime SET name = 'Jaime';") + + await self.connection.query("CREATE users:one SET name = 'one';") + await self.connection.query("CREATE users:two SET name = 'two';") + + outcome = await self.connection.select("user") + self.assertEqual( + outcome[0]["name"], + "Jaime", + ) + self.assertEqual( + outcome[1]["name"], + "Tobie", + ) + self.assertEqual(2, len(outcome)) + + await self.connection.query("DELETE user;") + await self.connection.query("DELETE users;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/select/test_blocking_http.py b/tests/unit_tests/connections/select/test_blocking_http.py new file mode 100644 index 00000000..6b080f8e --- /dev/null +++ b/tests/unit_tests/connections/select/test_blocking_http.py @@ -0,0 +1,48 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + + def test_select(self): + self.connection.query("DELETE user;") + self.connection.query("DELETE users;") + + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + self.connection.query("CREATE user:jaime SET name = 'Jaime';") + + self.connection.query("CREATE users:one SET name = 'one';") + self.connection.query("CREATE users:two SET name = 'two';") + + outcome = self.connection.select("user") + self.assertEqual( + outcome[0]["name"], + "Jaime", + ) + self.assertEqual( + outcome[1]["name"], + "Tobie", + ) + self.assertEqual(2, len(outcome)) + + self.connection.query("DELETE user;") + self.connection.query("DELETE users;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/select/test_blocking_ws.py b/tests/unit_tests/connections/select/test_blocking_ws.py new file mode 100644 index 00000000..c630dcdb --- /dev/null +++ b/tests/unit_tests/connections/select/test_blocking_ws.py @@ -0,0 +1,51 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + + def tearDown(self): + self.connection.query("DELETE user;") + self.connection.query("DELETE users;") + if self.connection.socket: + self.connection.socket.close() + + def test_select(self): + self.connection.query("DELETE user;") + self.connection.query("DELETE users;") + + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + self.connection.query("CREATE user:jaime SET name = 'Jaime';") + + self.connection.query("CREATE users:one SET name = 'one';") + self.connection.query("CREATE users:two SET name = 'two';") + + outcome = self.connection.select("user") + self.assertEqual( + outcome[0]["name"], + "Jaime", + ) + self.assertEqual( + outcome[1]["name"], + "Tobie", + ) + self.assertEqual(2, len(outcome)) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signin/__init__.py b/tests/unit_tests/connections/signin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/signin/test_async_http.py b/tests/unit_tests/connections/signin/test_async_http.py new file mode 100644 index 00000000..fb8c015c --- /dev/null +++ b/tests/unit_tests/connections/signin/test_async_http.py @@ -0,0 +1,95 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.database_name = "test_db" + self.namespace = "test_ns" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + _ = await self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = await self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + _ = await self.connection.query( + 'DEFINE USER test ON NAMESPACE PASSWORD "test" ROLES OWNER; ' + 'DEFINE USER test ON DATABASE PASSWORD "test" ROLES OWNER;' + ) + _ = await self.connection.query( + "CREATE user SET name = 'test', email = 'test@gmail.com', password = crypto::argon2::generate('test'), enabled = true" + ) + + async def test_signin_root(self): + connection = AsyncHttpSurrealConnection(self.url) + outcome = await connection.signin(self.vars_params) + self.assertIn("token", outcome) # Check that the response contains a token + + async def test_signin_namespace(self): + connection = AsyncHttpSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "username": "test", + "password": "test", + } + _ = await connection.signin(vars) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + + async def test_signin_database(self): + connection = AsyncHttpSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "database": self.database_name, + "username": "test", + "password": "test", + } + _ = await connection.signin(vars) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + + async def test_signin_record(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test" + } + } + connection = AsyncHttpSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = await connection.signin(vars) + + outcome = await connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + await self.connection.query("DELETE user;") + await self.connection.query("REMOVE TABLE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signin/test_async_ws.py b/tests/unit_tests/connections/signin/test_async_ws.py new file mode 100644 index 00000000..d8e0ba8c --- /dev/null +++ b/tests/unit_tests/connections/signin/test_async_ws.py @@ -0,0 +1,102 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.database_name = "test_db" + self.namespace = "test_ns" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + _ = await self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = await self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + _ = await self.connection.query( + 'DEFINE USER test ON NAMESPACE PASSWORD "test" ROLES OWNER; ' + 'DEFINE USER test ON DATABASE PASSWORD "test" ROLES OWNER;' + ) + _ = await self.connection.query( + "CREATE user SET name = 'test', email = 'test@gmail.com', password = crypto::argon2::generate('test'), enabled = true" + ) + + async def test_signin_root(self): + connection = AsyncWsSurrealConnection(self.url) + _ = await connection.signin(self.vars_params) + await self.connection.socket.close() + await connection.socket.close() + + async def test_signin_namespace(self): + connection = AsyncWsSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "username": "test", + "password": "test", + } + _ = await connection.signin(vars) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + await self.connection.socket.close() + await connection.socket.close() + + async def test_signin_database(self): + connection = AsyncWsSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "database": self.database_name, + "username": "test", + "password": "test", + } + _ = await connection.signin(vars) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + await self.connection.socket.close() + await connection.socket.close() + + async def test_signin_record(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test" + } + } + connection = AsyncWsSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = await connection.signin(vars) + + outcome = await connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + await self.connection.query("DELETE user;") + await self.connection.query("REMOVE TABLE user;") + await self.connection.socket.close() + await connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signin/test_blocking_http.py b/tests/unit_tests/connections/signin/test_blocking_http.py new file mode 100644 index 00000000..8f832fe6 --- /dev/null +++ b/tests/unit_tests/connections/signin/test_blocking_http.py @@ -0,0 +1,95 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.database_name = "test_db" + self.namespace = "test_ns" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + _ = self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + _ = self.connection.query( + 'DEFINE USER test ON NAMESPACE PASSWORD "test" ROLES OWNER; ' + 'DEFINE USER test ON DATABASE PASSWORD "test" ROLES OWNER;' + ) + _ = self.connection.query( + "CREATE user SET name = 'test', email = 'test@gmail.com', password = crypto::argon2::generate('test'), enabled = true" + ) + + def test_signin_root(self): + connection = BlockingHttpSurrealConnection(self.url) + outcome = connection.signin(self.vars_params) + self.assertIn("token", outcome) # Check that the response contains a token + + def test_signin_namespace(self): + connection = BlockingHttpSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "username": "test", + "password": "test", + } + _ = connection.signin(vars) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + + def test_signin_database(self): + connection = BlockingHttpSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "database": self.database_name, + "username": "test", + "password": "test", + } + _ = connection.signin(vars) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + + def test_signin_record(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test" + } + } + connection = BlockingHttpSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = connection.signin(vars) + + outcome = connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + self.connection.query("DELETE user;") + self.connection.query("REMOVE TABLE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signin/test_blocking_ws.py b/tests/unit_tests/connections/signin/test_blocking_ws.py new file mode 100644 index 00000000..bdc01851 --- /dev/null +++ b/tests/unit_tests/connections/signin/test_blocking_ws.py @@ -0,0 +1,102 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestAsyncHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.database_name = "test_db" + self.namespace = "test_ns" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.connection = BlockingWsSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + _ = self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + _ = self.connection.query( + 'DEFINE USER test ON NAMESPACE PASSWORD "test" ROLES OWNER; ' + 'DEFINE USER test ON DATABASE PASSWORD "test" ROLES OWNER;' + ) + _ = self.connection.query( + "CREATE user SET name = 'test', email = 'test@gmail.com', password = crypto::argon2::generate('test'), enabled = true" + ) + + def test_signin_root(self): + connection = BlockingWsSurrealConnection(self.url) + _ = connection.signin(self.vars_params) + self.connection.socket.close() + connection.close() + + def test_signin_namespace(self): + connection = BlockingWsSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "username": "test", + "password": "test", + } + _ = connection.signin(vars) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + self.connection.socket.close() + connection.socket.close() + + def test_signin_database(self): + connection = BlockingWsSurrealConnection(self.url) + vars = { + "namespace": self.namespace, + "database": self.database_name, + "username": "test", + "password": "test", + } + _ = connection.signin(vars) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + self.connection.socket.close() + connection.socket.close() + + def test_signin_record(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test" + } + } + connection = BlockingWsSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = connection.signin(vars) + + outcome = connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + self.connection.query("DELETE user;") + self.connection.query("REMOVE TABLE user;") + self.connection.socket.close() + connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signup/__init__.py b/tests/unit_tests/connections/signup/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/signup/test_async_http.py b/tests/unit_tests/connections/signup/test_async_http.py new file mode 100644 index 00000000..7d2ff3e4 --- /dev/null +++ b/tests/unit_tests/connections/signup/test_async_http.py @@ -0,0 +1,63 @@ +from unittest import TestCase, main, IsolatedAsyncioTestCase + +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + _ = await self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = await self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + + async def test_signup(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test", + "name": "test" + } + } + connection = AsyncHttpSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = await connection.signup(vars) + + outcome = await connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + await self.connection.query("DELETE user;") + await self.connection.query("REMOVE TABLE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signup/test_async_ws.py b/tests/unit_tests/connections/signup/test_async_ws.py new file mode 100644 index 00000000..e9ad96a1 --- /dev/null +++ b/tests/unit_tests/connections/signup/test_async_ws.py @@ -0,0 +1,63 @@ +from unittest import TestCase, main, IsolatedAsyncioTestCase + +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + _ = await self.connection.query("DELETE user;") + _ = await self.connection.query("REMOVE TABLE user;") + _ = await self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = await self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + + async def test_signup(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test", + "name": "test" + } + } + connection = AsyncWsSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = await connection.signup(vars) + + outcome = await connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + await self.connection.query("DELETE user;") + await self.connection.query("REMOVE TABLE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signup/test_blocking_http.py b/tests/unit_tests/connections/signup/test_blocking_http.py new file mode 100644 index 00000000..148a7ea6 --- /dev/null +++ b/tests/unit_tests/connections/signup/test_blocking_http.py @@ -0,0 +1,63 @@ +from unittest import TestCase, main + +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestBlockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + _ = self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + + def test_signup(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test", + "name": "test" + } + } + connection = BlockingHttpSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = connection.signup(vars) + + outcome = connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + self.connection.query("DELETE user;") + self.connection.query("REMOVE TABLE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/signup/test_blocking_ws.py b/tests/unit_tests/connections/signup/test_blocking_ws.py new file mode 100644 index 00000000..cdf01eb6 --- /dev/null +++ b/tests/unit_tests/connections/signup/test_blocking_ws.py @@ -0,0 +1,63 @@ +from unittest import TestCase, main + +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestAsyncWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + _ = self.connection.query("DELETE user;") + _ = self.connection.query("REMOVE TABLE user;") + _ = self.connection.query( + "DEFINE TABLE user SCHEMAFULL PERMISSIONS FOR select, update, delete WHERE id = $auth.id;" + "DEFINE FIELD name ON user TYPE string;" + "DEFINE FIELD email ON user TYPE string ASSERT string::is::email($value);" + "DEFINE FIELD password ON user TYPE string;" + "DEFINE FIELD enabled ON user TYPE bool;" + "DEFINE INDEX email ON user FIELDS email UNIQUE;" + ) + _ = self.connection.query( + 'DEFINE ACCESS user ON DATABASE TYPE RECORD ' + 'SIGNUP ( CREATE user SET name = $name, email = $email, password = crypto::argon2::generate($password), enabled = true ) ' + 'SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );' + ) + + def test_signup(self): + vars = { + "namespace": self.namespace, + "database": self.database_name, + "access": "user", + "variables": { + "email": "test@gmail.com", + "password": "test", + "name": "test" + } + } + connection = BlockingWsSurrealConnection(self.url) + # for below if client is HTTP then persist and attach to all headers + _ = connection.signup(vars) + + outcome = connection.info() + self.assertEqual(outcome["email"], "test@gmail.com") + self.assertEqual(outcome["name"], "test") + + self.connection.query("DELETE user;") + self.connection.query("REMOVE TABLE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/subscribe_live/__init__.py b/tests/unit_tests/connections/subscribe_live/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/subscribe_live/test_async_ws.py b/tests/unit_tests/connections/subscribe_live/test_async_ws.py new file mode 100644 index 00000000..3afdb882 --- /dev/null +++ b/tests/unit_tests/connections/subscribe_live/test_async_ws.py @@ -0,0 +1,58 @@ +import asyncio +from asyncio import TimeoutError +from unittest import main, IsolatedAsyncioTestCase +from uuid import UUID + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data import RecordID + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + await self.connection.signin(self.vars_params) + await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';") + self.pub_connection = AsyncWsSurrealConnection(self.url) + await self.pub_connection.signin(self.vars_params) + await self.pub_connection.use(namespace=self.namespace, database=self.database_name) + + async def test_live_subscription(self): + # Start the live query + query_uuid = await self.connection.live("user") + self.assertIsInstance(query_uuid, UUID) + + # Start the live subscription + subscription = self.connection.subscribe_live(query_uuid) + + # Push an update + await self.pub_connection.query("CREATE user:jaime SET name = 'Jaime';") + + try: + update = await asyncio.wait_for(subscription.__anext__(), timeout=10) + self.assertEqual(update["name"], "Jaime") + self.assertEqual(update["id"], RecordID("user", "jaime")) + except TimeoutError: + self.fail("Timed out waiting for live subscription update") + + await self.pub_connection.kill(query_uuid) + + # Cleanup the subscription + await self.pub_connection.query("DELETE user;") + await self.pub_connection.socket.close() + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/subscribe_live/test_blocking_ws.py b/tests/unit_tests/connections/subscribe_live/test_blocking_ws.py new file mode 100644 index 00000000..fcb64021 --- /dev/null +++ b/tests/unit_tests/connections/subscribe_live/test_blocking_ws.py @@ -0,0 +1,59 @@ +from unittest import TestCase, main +from uuid import UUID + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data import RecordID + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + self.pub_connection = BlockingWsSurrealConnection(self.url) + self.pub_connection.signin(self.vars_params) + self.pub_connection.use(namespace=self.namespace, database=self.database_name) + + def tearDown(self): + self.pub_connection.query("DELETE user;") + if self.pub_connection.socket: + self.pub_connection.socket.close() + if self.connection.socket: + self.connection.socket.close() + + def test_live_subscription(self): + # Start the live query + query_uuid = self.connection.live("user") + self.assertIsInstance(query_uuid, UUID) + + # Start the live subscription + subscription = self.connection.subscribe_live(query_uuid) + + # Push an update + self.pub_connection.query("CREATE user:jaime SET name = 'Jaime';") + + # Wait for the live subscription update + try: + for update in subscription: + self.assertEqual(update["name"], "Jaime") + self.assertEqual(update["id"], RecordID("user", "jaime")) + break # Exit after receiving the first update + except Exception as e: + self.fail(f"Error waiting for live subscription update: {e}") + + self.pub_connection.kill(query_uuid) + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/test_connection_constructor.py b/tests/unit_tests/connections/test_connection_constructor.py new file mode 100644 index 00000000..af86cd33 --- /dev/null +++ b/tests/unit_tests/connections/test_connection_constructor.py @@ -0,0 +1,37 @@ +from unittest import TestCase, main + +from surrealdb import Surreal, BlockingHttpSurrealConnection, BlockingWsSurrealConnection +from surrealdb import AsyncSurreal, AsyncHttpSurrealConnection, AsyncWsSurrealConnection + + +class TestUrl(TestCase): + + def setUp(self) -> None: + self.urls = [ + "http://localhost:5000", + "https://localhost:5000", + "http://localhost:5000/", + "https://localhost:5000/", + "ws://localhost:5000", + "wss://localhost:5000", + "ws://localhost:5000/", + "wss://localhost:5000/", + ] + self.schemes = ["http", "https", "http", "https", "ws", "wss", "ws", "wss"] + + def test_blocking___init__(self): + outcome = Surreal("ws://localhost:5000") + self.assertEqual(type(outcome), BlockingWsSurrealConnection) + + outcome = Surreal("http://localhost:5000") + self.assertEqual(type(outcome), BlockingHttpSurrealConnection) + + def test_async___init__(self): + outcome = AsyncSurreal("ws://localhost:5000") + self.assertEqual(type(outcome), AsyncWsSurrealConnection) + + outcome = AsyncSurreal("http://localhost:5000") + self.assertEqual(type(outcome), AsyncHttpSurrealConnection) + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/test_url.py b/tests/unit_tests/connections/test_url.py new file mode 100644 index 00000000..20befced --- /dev/null +++ b/tests/unit_tests/connections/test_url.py @@ -0,0 +1,31 @@ +from unittest import TestCase, main + +from surrealdb.connections.url import Url + + +class TestUrl(TestCase): + + def setUp(self) -> None: + self.urls = [ + "http://localhost:5000", + "https://localhost:5000", + "http://localhost:5000/", + "https://localhost:5000/", + "ws://localhost:5000", + "wss://localhost:5000", + "ws://localhost:5000/", + "wss://localhost:5000/", + ] + self.schemes = ["http", "https", "http", "https", "ws", "wss", "ws", "wss"] + + def test___init(self): + for x in range(len(self.urls)): + i = self.urls[x] + url = Url(i) + self.assertEqual(i, url.raw_url) + self.assertEqual(self.schemes[x], url.scheme.value) + self.assertEqual("localhost", url.hostname) + self.assertEqual(5000, url.port) + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/unset/__init__.py b/tests/unit_tests/connections/unset/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/unset/test_async_http.py b/tests/unit_tests/connections/unset/test_async_http.py new file mode 100644 index 00000000..9f741aaf --- /dev/null +++ b/tests/unit_tests/connections/unset/test_async_http.py @@ -0,0 +1,44 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_unset(self): + await self.connection.query("DELETE person;") + outcome = await self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertEqual(None, outcome) + await self.connection.query('CREATE person SET name = $name') + outcome = await self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual(1, len(outcome)) + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + + await self.connection.unset(key="name") + + # because the key was unset then $name.first is None returning [] + outcome = await self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual([], outcome) + + await self.connection.query("DELETE person;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/unset/test_async_ws.py b/tests/unit_tests/connections/unset/test_async_ws.py new file mode 100644 index 00000000..bb4633d1 --- /dev/null +++ b/tests/unit_tests/connections/unset/test_async_ws.py @@ -0,0 +1,45 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_unset(self): + await self.connection.query("DELETE person;") + outcome = await self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertEqual(None, outcome) + await self.connection.query('CREATE person SET name = $name') + outcome = await self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual(1, len(outcome)) + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + + await self.connection.unset(key="name") + + # because the key was unset then $name.first is None returning [] + outcome = await self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual([], outcome) + + await self.connection.query("DELETE person;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/unset/test_blocking_http.py b/tests/unit_tests/connections/unset/test_blocking_http.py new file mode 100644 index 00000000..88752953 --- /dev/null +++ b/tests/unit_tests/connections/unset/test_blocking_http.py @@ -0,0 +1,44 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestAsyncWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + + def test_unset(self): + self.connection.query("DELETE person;") + outcome = self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertEqual(None, outcome) + self.connection.query('CREATE person SET name = $name') + outcome = self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual(1, len(outcome)) + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + + self.connection.unset(key="name") + + # because the key was unset then $name.first is None returning [] + outcome = self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual([], outcome) + + self.connection.query("DELETE person;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/unset/test_blocking_ws.py b/tests/unit_tests/connections/unset/test_blocking_ws.py new file mode 100644 index 00000000..b1de2349 --- /dev/null +++ b/tests/unit_tests/connections/unset/test_blocking_ws.py @@ -0,0 +1,48 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + + def tearDown(self): + self.connection.query("DELETE person;") + if self.connection.socket: + self.connection.socket.close() + + def test_unset(self): + self.connection.query("DELETE person;") + outcome = self.connection.let('name', { + "first": 'Tobie', + "last": 'Morgan Hitchcock', + }) + self.assertIsNone(outcome) + + self.connection.query('CREATE person SET name = $name') + outcome = self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual(1, len(outcome)) + self.assertEqual({'first': 'Tobie', 'last': 'Morgan Hitchcock'}, outcome[0]["name"]) + + self.connection.unset(key="name") + + # Because the key was unset, $name.first is None, returning [] + outcome = self.connection.query('SELECT * FROM person WHERE name.first = $name.first') + self.assertEqual([], outcome) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/update/__init__.py b/tests/unit_tests/connections/update/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/update/test_async_http.py b/tests/unit_tests/connections/update/test_async_http.py new file mode 100644 index 00000000..faaf5088 --- /dev/null +++ b/tests/unit_tests/connections/update/test_async_http.py @@ -0,0 +1,97 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_update_string(self): + outcome = await self.connection.update("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_update_string_with_data(self): + first_outcome = await self.connection.update("user:tobie", self.data) + self.check_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_update_record_id(self): + first_outcome = await self.connection.update(self.record_id) + self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_update_record_id_with_data(self): + outcome = await self.connection.update(self.record_id, self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change( + outcome[0] + ) + await self.connection.query("DELETE user;") + + async def test_update_table(self): + table = Table("user") + first_outcome = await self.connection.update(table) + self.check_no_change(first_outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + await self.connection.query("DELETE user;") + + async def test_update_table_with_data(self): + table = Table("user") + outcome = await self.connection.update(table, self.data) + self.check_change(outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/update/test_async_ws.py b/tests/unit_tests/connections/update/test_async_ws.py new file mode 100644 index 00000000..0452d59b --- /dev/null +++ b/tests/unit_tests/connections/update/test_async_ws.py @@ -0,0 +1,103 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_update_string(self): + outcome = await self.connection.update("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_update_string_with_data(self): + first_outcome = await self.connection.update("user:tobie", self.data) + self.check_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_update_record_id(self): + first_outcome = await self.connection.update(self.record_id) + self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_update_record_id_with_data(self): + outcome = await self.connection.update(self.record_id, self.data) + self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change( + outcome[0] + ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_update_table(self): + table = Table("user") + first_outcome = await self.connection.update(table) + self.check_no_change(first_outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_update_table_with_data(self): + table = Table("user") + outcome = await self.connection.update(table, self.data) + self.check_change(outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/update/test_blocking_http.py b/tests/unit_tests/connections/update/test_blocking_http.py new file mode 100644 index 00000000..5bfd0eba --- /dev/null +++ b/tests/unit_tests/connections/update/test_blocking_http.py @@ -0,0 +1,97 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_update_string(self): + outcome = self.connection.update("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_update_string_with_data(self): + first_outcome = self.connection.update("user:tobie", self.data) + self.check_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_update_record_id(self): + first_outcome = self.connection.update(self.record_id) + self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_update_record_id_with_data(self): + outcome = self.connection.update(self.record_id, self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change( + outcome[0] + ) + self.connection.query("DELETE user;") + + def test_update_table(self): + table = Table("user") + first_outcome = self.connection.update(table) + self.check_no_change(first_outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + self.connection.query("DELETE user;") + + def test_update_table_with_data(self): + table = Table("user") + outcome = self.connection.update(table, self.data) + self.check_change(outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/update/test_blocking_ws.py b/tests/unit_tests/connections/update/test_blocking_ws.py new file mode 100644 index 00000000..f17f2bc6 --- /dev/null +++ b/tests/unit_tests/connections/update/test_blocking_ws.py @@ -0,0 +1,92 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def check_no_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual("Tobie", data["name"]) + + def check_change(self, data: dict): + self.assertEqual(self.record_id, data["id"]) + self.assertEqual("Jaime", data["name"]) + self.assertEqual(35, data["age"]) + + def test_update_string(self): + outcome = self.connection.update("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + def test_update_string_with_data(self): + first_outcome = self.connection.update("user:tobie", self.data) + self.check_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + def test_update_record_id(self): + first_outcome = self.connection.update(self.record_id) + self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + def test_update_record_id_with_data(self): + outcome = self.connection.update(self.record_id, self.data) + self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + def test_update_table(self): + table = Table("user") + first_outcome = self.connection.update(table) + self.check_no_change(first_outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_no_change(outcome[0]) + + def test_update_table_with_data(self): + table = Table("user") + outcome = self.connection.update(table, self.data) + self.check_change(outcome[0]) + outcome = self.connection.query("SELECT * FROM user;") + self.check_change(outcome[0]) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/upsert/__init__.py b/tests/unit_tests/connections/upsert/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/upsert/test_async_http.py b/tests/unit_tests/connections/upsert/test_async_http.py new file mode 100644 index 00000000..5cad4eab --- /dev/null +++ b/tests/unit_tests/connections/upsert/test_async_http.py @@ -0,0 +1,99 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncHttpSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_upsert_string(self): + outcome = await self.connection.upsert("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_upsert_string_with_data(self): + first_outcome = await self.connection.upsert("user:tobie", self.data) + # self.check_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_upsert_record_id(self): + first_outcome = await self.connection.upsert(self.record_id) + # self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + + async def test_upsert_record_id_with_data(self): + outcome = await self.connection.upsert(self.record_id, self.data) + # self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_change( + # outcome[0] + # ) + await self.connection.query("DELETE user;") + + async def test_upsert_table(self): + table = Table("user") + first_outcome = await self.connection.upsert(table) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_no_change(outcome[1], random_id=True) + + await self.connection.query("DELETE user;") + + async def test_upsert_table_with_data(self): + table = Table("user") + outcome = await self.connection.upsert(table, self.data) + # self.check_change(outcome[0], random_id=True) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_change(outcome[0], random_id=True) + await self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/upsert/test_async_ws.py b/tests/unit_tests/connections/upsert/test_async_ws.py new file mode 100644 index 00000000..5bcd949b --- /dev/null +++ b/tests/unit_tests/connections/upsert/test_async_ws.py @@ -0,0 +1,106 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + await self.connection.query("DELETE user;") + await self.connection.query("CREATE user:tobie SET name = 'Tobie';"), + + def check_no_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + async def test_upsert_string(self): + outcome = await self.connection.upsert("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_upsert_string_with_data(self): + first_outcome = await self.connection.upsert("user:tobie", self.data) + # self.check_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_upsert_record_id(self): + first_outcome = await self.connection.upsert(self.record_id) + # self.check_no_change(first_outcome) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_upsert_record_id_with_data(self): + outcome = await self.connection.upsert(self.record_id, self.data) + # self.check_change(outcome) + outcome = await self.connection.query("SELECT * FROM user;") + # self.check_change( + # outcome[0] + # ) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_upsert_table(self): + table = Table("user") + first_outcome = await self.connection.upsert(table) + # self.check_no_change(first_outcome[0]) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_no_change(outcome[1], random_id=True) + + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + async def test_upsert_table_with_data(self): + table = Table("user") + outcome = await self.connection.upsert(table, self.data) + # self.check_change(outcome[0], random_id=True) + outcome = await self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_change(outcome[0], random_id=True) + await self.connection.query("DELETE user;") + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/upsert/test_blocking_http.py b/tests/unit_tests/connections/upsert/test_blocking_http.py new file mode 100644 index 00000000..f575aff7 --- /dev/null +++ b/tests/unit_tests/connections/upsert/test_blocking_http.py @@ -0,0 +1,101 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingHttpSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def check_no_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_upsert_string(self): + outcome = self.connection.upsert("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(1, len(outcome)) + # self.check_no_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_upsert_string_with_data(self): + first_outcome = self.connection.upsert("user:tobie", self.data) + # self.check_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_upsert_record_id(self): + first_outcome = self.connection.upsert(self.record_id) + # self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + self.connection.query("DELETE user;") + + def test_upsert_record_id_with_data(self): + outcome = self.connection.upsert(self.record_id, self.data) + # self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_change( + # outcome[0] + # ) + self.connection.query("DELETE user;") + + def test_upsert_table(self): + table = Table("user") + first_outcome = self.connection.upsert(table) + # self.check_no_change(first_outcome[0], random_id=True) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_no_change(outcome[1], random_id=True) + + self.connection.query("DELETE user;") + + def test_upsert_table_with_data(self): + table = Table("user") + first_outcome = self.connection.upsert(table, self.data) + # self.check_change(first_outcome[0], random_id=True) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_no_change(outcome[1], random_id=True) + self.connection.query("DELETE user;") + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/upsert/test_blocking_ws.py b/tests/unit_tests/connections/upsert/test_blocking_ws.py new file mode 100644 index 00000000..ff3781c1 --- /dev/null +++ b/tests/unit_tests/connections/upsert/test_blocking_ws.py @@ -0,0 +1,96 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection +from surrealdb.data.types.record_id import RecordID +from surrealdb.data.types.table import Table + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.data = { + "name": "Jaime", + "age": 35 + } + self.record_id = RecordID("user", "tobie") + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + self.connection.query("DELETE user;") + self.connection.query("CREATE user:tobie SET name = 'Tobie';") + + def tearDown(self): + self.connection.query("DELETE user;") + if self.connection.socket: + self.connection.socket.close() + + def check_no_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Tobie', data["name"]) + + def check_change(self, data: dict, random_id: bool = False): + if random_id is False: + self.assertEqual(self.record_id, data["id"]) + self.assertEqual('Jaime', data["name"]) + self.assertEqual(35, data["age"]) + + def test_upsert_string(self): + outcome = self.connection.upsert("user:tobie") + self.assertEqual( + outcome["id"], + self.record_id + ) + self.assertEqual( + outcome["name"], + "Tobie" + ) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + + def test_upsert_string_with_data(self): + first_outcome = self.connection.upsert("user:tobie", self.data) + # self.check_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_change(outcome[0]) + + def test_upsert_record_id(self): + first_outcome = self.connection.upsert(self.record_id) + # self.check_no_change(first_outcome) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_no_change(outcome[0]) + + def test_upsert_record_id_with_data(self): + outcome = self.connection.upsert(self.record_id, self.data) + # self.check_change(outcome) + outcome = self.connection.query("SELECT * FROM user;") + # self.check_change(outcome[0]) + + def test_upsert_table(self): + table = Table("user") + first_outcome = self.connection.upsert(table) + # self.check_no_change(first_outcome[0], random_id=True) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_no_change(outcome[1], random_id=True) + + def test_upsert_table_with_data(self): + table = Table("user") + outcome = self.connection.upsert(table, self.data) + # self.check_change(outcome[0], random_id=True) + outcome = self.connection.query("SELECT * FROM user;") + self.assertEqual(2, len(outcome)) + # self.check_change(outcome[0], random_id=True) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/version/__init__.py b/tests/unit_tests/connections/version/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/connections/version/test_async_http.py b/tests/unit_tests/connections/version/test_async_http.py new file mode 100644 index 00000000..bf2388b8 --- /dev/null +++ b/tests/unit_tests/connections/version/test_async_http.py @@ -0,0 +1,27 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_http import AsyncHttpSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncHttpSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_version(self): + self.assertEqual(str, type(await self.connection.version())) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/version/test_async_ws.py b/tests/unit_tests/connections/version/test_async_ws.py new file mode 100644 index 00000000..5e602d60 --- /dev/null +++ b/tests/unit_tests/connections/version/test_async_ws.py @@ -0,0 +1,28 @@ +from unittest import main, IsolatedAsyncioTestCase + +from surrealdb.connections.async_ws import AsyncWsSurrealConnection + + +class TestAsyncWsSurrealConnection(IsolatedAsyncioTestCase): + + async def asyncSetUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = AsyncWsSurrealConnection(self.url) + _ = await self.connection.signin(self.vars_params) + _ = await self.connection.use(namespace=self.namespace, database=self.database_name) + + async def test_version(self): + self.assertEqual(str, type(await self.connection.version())) + await self.connection.socket.close() + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/version/test_blocking_http.py b/tests/unit_tests/connections/version/test_blocking_http.py new file mode 100644 index 00000000..56f633df --- /dev/null +++ b/tests/unit_tests/connections/version/test_blocking_http.py @@ -0,0 +1,27 @@ +from unittest import main, TestCase + +from surrealdb.connections.blocking_http import BlockingHttpSurrealConnection + + +class TestAsyncWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "http://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingHttpSurrealConnection(self.url) + _ = self.connection.signin(self.vars_params) + _ = self.connection.use(namespace=self.namespace, database=self.database_name) + + def test_version(self): + self.assertEqual(str, type(self.connection.version())) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/connections/version/test_blocking_ws.py b/tests/unit_tests/connections/version/test_blocking_ws.py new file mode 100644 index 00000000..e9f94023 --- /dev/null +++ b/tests/unit_tests/connections/version/test_blocking_ws.py @@ -0,0 +1,31 @@ +from unittest import TestCase, main + +from surrealdb.connections.blocking_ws import BlockingWsSurrealConnection + + +class TestBlockingWsSurrealConnection(TestCase): + + def setUp(self): + self.url = "ws://localhost:8000" + self.password = "root" + self.username = "root" + self.vars_params = { + "username": self.username, + "password": self.password, + } + self.database_name = "test_db" + self.namespace = "test_ns" + self.connection = BlockingWsSurrealConnection(self.url) + self.connection.signin(self.vars_params) + self.connection.use(namespace=self.namespace, database=self.database_name) + + def tearDown(self): + if self.connection.socket: + self.connection.socket.close() + + def test_version(self): + self.assertEqual(str, type(self.connection.version())) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/request_message/__init__.py b/tests/unit_tests/request_message/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/request_message/descriptors/__init__.py b/tests/unit_tests/request_message/descriptors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit_tests/request_message/descriptors/test_cbor_ws.py b/tests/unit_tests/request_message/descriptors/test_cbor_ws.py new file mode 100644 index 00000000..2db08555 --- /dev/null +++ b/tests/unit_tests/request_message/descriptors/test_cbor_ws.py @@ -0,0 +1,229 @@ +from unittest import TestCase, main + +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class TestWsCborAdapter(TestCase): + + def test_use_pass(self): + message = RequestMessage(1, RequestMethod.USE, namespace="ns", database="db") + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_use_fail(self): + message = RequestMessage(1, RequestMethod.USE, namespace="ns", database=1) + with self.assertRaises(ValueError) as context: + message.WS_CBOR_DESCRIPTOR + self.assertEqual( + "Invalid schema for Cbor WS encoding for use: {'params': [{1: ['must be of string type']}]}", + str(context.exception) + ) + message = RequestMessage(1, RequestMethod.USE, namespace="ns") + with self.assertRaises(ValueError) as context: + message.WS_CBOR_DESCRIPTOR + self.assertEqual( + "Invalid schema for Cbor WS encoding for use: {'params': [{1: ['null value not allowed']}]}", + str(context.exception) + ) + + def test_info_pass(self): + message = RequestMessage(1, RequestMethod.INFO) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_version_pass(self): + message = RequestMessage(1, RequestMethod.VERSION) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_signin_pass_root(self): + message = RequestMessage( + 1, + RequestMethod.SIGN_IN, + username="user", + password="pass" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_signin_pass_root_with_none(self): + message = RequestMessage( + 1, + RequestMethod.SIGN_IN, + username="username", + password="pass", + account=None, + database=None, + namespace=None + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_signin_pass_account(self): + message = RequestMessage( + 1, + RequestMethod.SIGN_IN, + username="username", + password="pass", + account="account", + database="database", + namespace="namespace" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_authenticate_pass(self): + message = RequestMessage( + 1, + RequestMethod.AUTHENTICATE, + token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJTdXJyZWFsREIiLCJpYXQiOjE1MTYyMzkwMjIsIm5iZiI6MTUxNjIzOTAyMiwiZXhwIjoxODM2NDM5MDIyLCJOUyI6InRlc3QiLCJEQiI6InRlc3QiLCJTQyI6InVzZXIiLCJJRCI6InVzZXI6dG9iaWUifQ.N22Gp9ze0rdR06McGj1G-h2vu6a6n9IVqUbMFJlOxxA" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_invalidate_pass(self): + message = RequestMessage( + 1, + RequestMethod.INVALIDATE + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_let_pass(self): + message = RequestMessage( + 1, + RequestMethod.LET, + key="key", + value="value" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_unset_pass(self): + message = RequestMessage( + 1, + RequestMethod.UNSET, + params=["one", "two", "three"] + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_live_pass(self): + message = RequestMessage( + 1, + RequestMethod.LIVE, + table="person" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_kill_pass(self): + message = RequestMessage( + 1, + RequestMethod.KILL, + uuid="0189d6e3-8eac-703a-9a48-d9faa78b44b9" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_query_pass(self): + message = RequestMessage( + 1, + RequestMethod.QUERY, + query="query" + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_create_pass_params(self): + message = RequestMessage( + 1, + RequestMethod.CREATE, + collection="person", + data={"table": "table"} + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_insert_pass_dict(self): + message = RequestMessage( + 1, + RequestMethod.INSERT, + collection="table", + params={"key": "value"} + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_insert_pass_list(self): + message = RequestMessage( + 1, + RequestMethod.INSERT, + collection="table", + params=[{"key": "value"}, {"key": "value"}] + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_patch_pass(self): + message = RequestMessage( + 1, + RequestMethod.PATCH, + collection="table", + params=[{"key": "value"}, {"key": "value"}] + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_select_pass(self): + message = RequestMessage( + 1, + RequestMethod.SELECT, + params=["table", "user"], + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_update_pass(self): + message = RequestMessage( + 1, + RequestMethod.UPDATE, + record_id="test", + data={"table": "table"} + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_upsert_pass(self): + message = RequestMessage( + 1, + RequestMethod.UPSERT, + record_id="test", + data={"table": "table"} + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_merge_pass(self): + message = RequestMessage( + 1, + RequestMethod.MERGE, + record_id="test", + data={"table": "table"} + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + def test_delete_pass(self): + message = RequestMessage( + 1, + RequestMethod.DELETE, + record_id="test", + ) + outcome = message.WS_CBOR_DESCRIPTOR + self.assertIsInstance(outcome, bytes) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/unit_tests/request_message/descriptors/test_json_http.py b/tests/unit_tests/request_message/descriptors/test_json_http.py new file mode 100644 index 00000000..5de56152 --- /dev/null +++ b/tests/unit_tests/request_message/descriptors/test_json_http.py @@ -0,0 +1,57 @@ +from unittest import TestCase, main + +from surrealdb.request_message.descriptors.json_http import HttpMethod +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class TestJsonHttpDescriptor(TestCase): + + def test_signin_pass_root(self): + message = RequestMessage( + 1, + RequestMethod.SIGN_IN, + username="user", + password="pass" + ) + json_body, method, endpoint = message.JSON_HTTP_DESCRIPTOR + self.assertEqual('{"user": "user", "pass": "pass"}', json_body) + self.assertEqual(HttpMethod.POST, method) + self.assertEqual("signin", endpoint) + + def test_signin_pass_root_with_none(self): + message = RequestMessage( + 1, + RequestMethod.SIGN_IN, + username="username", + password="pass", + account=None, + database=None, + namespace=None + ) + json_body, method, endpoint = message.JSON_HTTP_DESCRIPTOR + self.assertEqual('{"user": "username", "pass": "pass"}', json_body) + self.assertEqual(HttpMethod.POST, method) + self.assertEqual("signin", endpoint) + + def test_signin_pass_account(self): + message = RequestMessage( + 1, + RequestMethod.SIGN_IN, + username="username", + password="pass", + account="account", + database="database", + namespace="namespace" + ) + json_body, method, endpoint = message.JSON_HTTP_DESCRIPTOR + self.assertEqual( + '{"ns": "namespace", "db": "database", "ac": "account", "user": "username", "pass": "pass"}', + json_body + ) + self.assertEqual(HttpMethod.POST, method) + self.assertEqual("signin", endpoint) + + +if __name__ == '__main__': + main() diff --git a/tests/unit_tests/request_message/test.sql b/tests/unit_tests/request_message/test.sql new file mode 100644 index 00000000..adaee54b --- /dev/null +++ b/tests/unit_tests/request_message/test.sql @@ -0,0 +1,7 @@ +-- this is a test file +CREATE user:tobie SET name = 'Tobie'; +-- another comment +CREATE user:jaime SET name = 'Jaime'; + +CREATE user:three +SET name = 'Three'; diff --git a/tests/unit_tests/request_message/test_adapter.py b/tests/unit_tests/request_message/test_adapter.py new file mode 100644 index 00000000..3f4bb3e0 --- /dev/null +++ b/tests/unit_tests/request_message/test_adapter.py @@ -0,0 +1,57 @@ +import os +from unittest import TestCase, main +from surrealdb.request_message.sql_adapter import SqlAdapter + + +class TestSqlAdapter(TestCase): + + def setUp(self): + self.expected_sql = "CREATE user:tobie SET name = 'Tobie'; CREATE user:jaime SET name = 'Jaime';" + + def test_from_docstring(self): + query = """ + CREATE user:tobie SET name = 'Tobie'; + CREATE user:jaime SET name = 'Jaime'; + """ + sql = SqlAdapter.from_docstring(query) + self.assertEqual(self.expected_sql, sql) + query = """ + + + CREATE user:tobie SET name = 'Tobie'; + + CREATE + user:jaime + SET name = 'Jaime'; + + + """ + sql = SqlAdapter.from_docstring(query) + self.assertEqual(self.expected_sql, sql) + + def test_from_list(self): + query = [ + "CREATE user:tobie SET name = 'Tobie';", + "", + "CREATE user:jaime SET name = 'Jaime'" + ] + sql = SqlAdapter.from_list(query) + self.assertEqual(self.expected_sql, sql) + + def test_from_file(self): + current_file_path = os.path.abspath(__file__) + + # Get the directory of the current file + directory = os.path.dirname(current_file_path) + file_path = os.path.join(directory, "test.sql") + sql = SqlAdapter.from_file(str(file_path)) + expected_sql = ("" + "CREATE user:tobie SET name = 'Tobie'; " + "CREATE user:jaime SET name = 'Jaime'; " + "CREATE user:three SET name = 'Three';" + ) + self.assertEqual(expected_sql, sql) + + +if __name__ == "__main__": + main() diff --git a/tests/unit_tests/request_message/test_request_message.py b/tests/unit_tests/request_message/test_request_message.py new file mode 100644 index 00000000..90c87541 --- /dev/null +++ b/tests/unit_tests/request_message/test_request_message.py @@ -0,0 +1,19 @@ +from unittest import TestCase, main +from surrealdb.request_message.message import RequestMessage +from surrealdb.request_message.methods import RequestMethod + + +class TestRequestMessage(TestCase): + + def setUp(self): + self.method = RequestMethod.USE + + def test_init(self): + request_message = RequestMessage(1, self.method, one="two", three="four") + + self.assertEqual(request_message.method, self.method) + self.assertEqual(request_message.kwargs, {"one": "two", "three": "four"}) + + +if __name__ == "__main__": + main()