ekodb-client


Nameekodb-client JSON
Version 0.3.0 PyPI version JSON
download
home_pageNone
SummaryPython client for ekoDB - a high-performance document database
upload_time2025-11-05 06:11:35
maintainerNone
docs_urlNone
authorekoDB Team
requires_python>=3.8
licenseMIT OR Apache-2.0
keywords database ekodb document-database nosql
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # ekoDB Python Client

High-performance Python client for ekoDB, built with Rust for speed and safety.

This package wraps the `ekodb_client` Rust library using PyO3 to provide a
native Python interface.

## Features

- ✅ **Fast**: Built with Rust, leveraging the same client library as the Rust
  SDK
- ✅ **Type-safe**: Strong typing with Python type hints
- ✅ **Async/await**: Full async support using Python's asyncio
- ✅ **Easy to use**: Pythonic API that feels natural
- ✅ **Complete**: All ekoDB features supported
- ✅ **Query Builder** - Fluent API for complex queries with operators, sorting,
  and pagination
- ✅ **Search** - Full-text search, fuzzy search, and field-specific search with
  scoring
- ✅ **Schema Management** - Define and enforce data schemas with validation
- ✅ **Join Operations** - Single and multi-collection joins with queries
- ✅ **Rate limiting with automatic retry** (429, 503, network errors)
- ✅ **Rate limit tracking** (`X-RateLimit-*` headers)
- ✅ **Configurable retry behavior**
- ✅ **Retry-After header support**

## Installation

```bash
pip install ekodb
```

Or install from source:

```bash
cd ekodb-py
pip install maturin
maturin develop
```

## Quick Start

```python
import asyncio
from ekodb_client import Client, RateLimitError

async def main():
    # Create client with configuration
    client = Client.new(
        "http://localhost:8080",
        "your-api-key",
        should_retry=True,  # Enable automatic retries (default: True)
        max_retries=3,      # Maximum retry attempts (default: 3)
        timeout_secs=30     # Request timeout in seconds (default: 30)
    )

    try:
        # Insert a document
        record = await client.insert("users", {
            "name": "John Doe",
            "age": 30,
            "email": "john@example.com",
            "active": True
        })
        print(f"Inserted: {record['id']}")

        # Find by ID
        user = await client.find_by_id("users", record["id"])
        print(f"Found: {user}")

        # Find with query
        results = await client.find("users", limit=10)
        print(f"Found {len(results)} users")

        # Update
        updated = await client.update("users", record["id"], {
            "age": 31
        })
        print(f"Updated: {updated}")

        # Delete
        await client.delete("users", record["id"])
        print("Deleted")

    except RateLimitError as e:
        print(f"Rate limited! Retry after {e.retry_after_secs} seconds")

asyncio.run(main())
```

## Usage Examples

### Query Builder

```python
from ekodb_client import Client, QueryBuilder

async def main():
    client = Client.new("http://localhost:8080", "your-api-key")

    # Simple query with operators
    query = QueryBuilder() \
        .eq("status", "active") \
        .gte("age", 18) \
        .lt("age", 65) \
        .limit(10) \
        .build()

    results = await client.find("users", query)

    # Complex query with sorting and pagination
    query = QueryBuilder() \
        .in_array("status", ["active", "pending"]) \
        .contains("email", "@example.com") \
        .sort_desc("created_at") \
        .skip(20) \
        .limit(10) \
        .build()

    results = await client.find("users", query)
```

### Search Operations

```python
# Basic text search
search_query = {
    "query": "programming",
    "min_score": 0.1,
    "limit": 10
}

results = await client.search("articles", search_query)
for result in results["results"]:
    print(f"Score: {result['score']:.4f} - {result['record']['title']}")

# Search with field weights
search_query = {
    "query": "rust database",
    "fields": ["title", "description"],
    "weights": {"title": 2.0},
    "limit": 5
}

results = await client.search("articles", search_query)
```

### Schema Management

```python
# Create a collection with schema
schema = {
    "fields": {
        "name": {
            "field_type": "String",
            "required": True,
            "regex": "^[a-zA-Z ]+$"
        },
        "email": {
            "field_type": "String",
            "required": True,
            "unique": True
        },
        "age": {
            "field_type": "Integer",
            "min": 0,
            "max": 150
        }
    }
}

await client.create_collection("users", schema)

# Get collection schema
schema = await client.get_schema("users")
```

### Join Operations

```python
# Single collection join
query = {
    "join": {
        "collections": ["departments"],
        "local_field": "department_id",
        "foreign_field": "id",
        "as_field": "department"
    },
    "limit": 10
}

results = await client.find("users", query)

# Multi-collection join
query = {
    "join": [
        {
            "collections": ["departments"],
            "local_field": "department_id",
            "foreign_field": "id",
            "as_field": "department"
        },
        {
            "collections": ["profiles"],
            "local_field": "id",
            "foreign_field": "id",
            "as_field": "profile"
        }
    ],
    "limit": 10
}

results = await client.find("users", query)
```

## API Reference

### Client

#### `Client.new(base_url: str, api_key: str, should_retry: bool = True, max_retries: int = 3, timeout_secs: int = 30) -> Client`

Create a new ekoDB client.

**Parameters:**

- `base_url`: The base URL of the ekoDB server
- `api_key`: Your API key
- `should_retry`: Enable automatic retries (default: True)
- `max_retries`: Maximum number of retry attempts (default: 3)
- `timeout_secs`: Request timeout in seconds (default: 30)

**Returns:**

- A new `Client` instance

### RateLimitInfo

Rate limit information is automatically tracked and logged by the client. The
client will automatically retry on rate limit errors using the server's
`Retry-After` header.

#### Properties

- `limit: int` - Maximum requests allowed per window
- `remaining: int` - Requests remaining in current window
- `reset: int` - Unix timestamp when the rate limit resets

#### Methods

- `is_near_limit() -> bool` - Check if approaching rate limit (<10% remaining)
- `is_exceeded() -> bool` - Check if the rate limit has been exceeded
- `remaining_percentage() -> float` - Get the percentage of requests remaining

### RateLimitError

Exception raised when rate limit is exceeded (if retries are disabled or
exhausted).

#### Properties

- `retry_after_secs: int` - Number of seconds to wait before retrying

#### `await client.insert(collection: str, record: dict) -> dict`

Insert a document into a collection.

**Parameters:**

- `collection`: The collection name
- `record`: A dictionary representing the document

**Returns:**

- The inserted document with ID

#### `await client.find_by_id(collection: str, id: str) -> dict`

Find a document by ID.

**Parameters:**

- `collection`: The collection name
- `id`: The document ID

**Returns:**

- The found document

#### `await client.find(collection: str, limit: Optional[int] = None) -> List[dict]`

Find documents in a collection.

**Parameters:**

- `collection`: The collection name
- `limit`: Optional limit on number of results

**Returns:**

- List of matching documents

#### `await client.update(collection: str, id: str, updates: dict) -> dict`

Update a document.

**Parameters:**

- `collection`: The collection name
- `id`: The document ID
- `updates`: Dictionary of fields to update

**Returns:**

- The updated document

#### `await client.delete(collection: str, id: str) -> None`

Delete a document.

**Parameters:**

- `collection`: The collection name
- `id`: The document ID

#### `await client.list_collections() -> List[str]`

List all collections.

**Returns:**

- List of collection names

#### `await client.delete_collection(collection: str) -> None`

Delete a collection.

**Parameters:**

- `collection`: The collection name to delete

#### `await client.search(collection: str, query: dict) -> dict`

Perform full-text search on a collection.

**Parameters:**

- `collection`: The collection name
- `query`: Search query dictionary with fields like `query`, `fields`,
  `weights`, `min_score`, `limit`

**Returns:**

- Search results with scores and matched records

#### `await client.create_collection(collection: str, schema: dict) -> None`

Create a collection with a schema.

**Parameters:**

- `collection`: The collection name
- `schema`: Schema definition dictionary

#### `await client.get_schema(collection: str) -> dict`

Get the schema for a collection.

**Parameters:**

- `collection`: The collection name

**Returns:**

- Schema definition dictionary

#### `await client.get_collection(collection: str) -> dict`

Get collection metadata including schema.

**Parameters:**

- `collection`: The collection name

**Returns:**

- Collection metadata dictionary

## Examples

See the
[examples directory](https://github.com/ekoDB/ekodb-client/tree/main/examples/python)
for complete working examples:

- `client_simple_crud.py` - Basic CRUD operations
- `client_query_builder.py` - Complex queries with QueryBuilder
- `client_search.py` - Full-text search operations
- `client_schema.py` - Schema management
- `client_joins.py` - Join operations
- `client_batch_operations.py` - Batch operations
- `client_kv_operations.py` - Key-value operations
- And more...

## Development

### Building

```bash
# Install maturin
pip install maturin

# Build and install in development mode
maturin develop

# Build release wheel
maturin build --release
```

### Testing

```bash
# Run Python tests
pytest

# Run with coverage
pytest --cov=ekodb
```

## License

MIT OR Apache-2.0

## Links

- [GitHub](https://github.com/ekoDB/ekodb-client)
- [Examples](https://github.com/ekoDB/ekodb-client/tree/main/examples/python)
- [PyPI Package](https://pypi.org/project/ekodb-client/)


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "ekodb-client",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "database, ekodb, document-database, nosql",
    "author": "ekoDB Team",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/85/27/8993556edbb5a65c2563fef5449ce8696955584277d38b1efb32f54aee3f/ekodb_client-0.3.0.tar.gz",
    "platform": null,
    "description": "# ekoDB Python Client\n\nHigh-performance Python client for ekoDB, built with Rust for speed and safety.\n\nThis package wraps the `ekodb_client` Rust library using PyO3 to provide a\nnative Python interface.\n\n## Features\n\n- \u2705 **Fast**: Built with Rust, leveraging the same client library as the Rust\n  SDK\n- \u2705 **Type-safe**: Strong typing with Python type hints\n- \u2705 **Async/await**: Full async support using Python's asyncio\n- \u2705 **Easy to use**: Pythonic API that feels natural\n- \u2705 **Complete**: All ekoDB features supported\n- \u2705 **Query Builder** - Fluent API for complex queries with operators, sorting,\n  and pagination\n- \u2705 **Search** - Full-text search, fuzzy search, and field-specific search with\n  scoring\n- \u2705 **Schema Management** - Define and enforce data schemas with validation\n- \u2705 **Join Operations** - Single and multi-collection joins with queries\n- \u2705 **Rate limiting with automatic retry** (429, 503, network errors)\n- \u2705 **Rate limit tracking** (`X-RateLimit-*` headers)\n- \u2705 **Configurable retry behavior**\n- \u2705 **Retry-After header support**\n\n## Installation\n\n```bash\npip install ekodb\n```\n\nOr install from source:\n\n```bash\ncd ekodb-py\npip install maturin\nmaturin develop\n```\n\n## Quick Start\n\n```python\nimport asyncio\nfrom ekodb_client import Client, RateLimitError\n\nasync def main():\n    # Create client with configuration\n    client = Client.new(\n        \"http://localhost:8080\",\n        \"your-api-key\",\n        should_retry=True,  # Enable automatic retries (default: True)\n        max_retries=3,      # Maximum retry attempts (default: 3)\n        timeout_secs=30     # Request timeout in seconds (default: 30)\n    )\n\n    try:\n        # Insert a document\n        record = await client.insert(\"users\", {\n            \"name\": \"John Doe\",\n            \"age\": 30,\n            \"email\": \"john@example.com\",\n            \"active\": True\n        })\n        print(f\"Inserted: {record['id']}\")\n\n        # Find by ID\n        user = await client.find_by_id(\"users\", record[\"id\"])\n        print(f\"Found: {user}\")\n\n        # Find with query\n        results = await client.find(\"users\", limit=10)\n        print(f\"Found {len(results)} users\")\n\n        # Update\n        updated = await client.update(\"users\", record[\"id\"], {\n            \"age\": 31\n        })\n        print(f\"Updated: {updated}\")\n\n        # Delete\n        await client.delete(\"users\", record[\"id\"])\n        print(\"Deleted\")\n\n    except RateLimitError as e:\n        print(f\"Rate limited! Retry after {e.retry_after_secs} seconds\")\n\nasyncio.run(main())\n```\n\n## Usage Examples\n\n### Query Builder\n\n```python\nfrom ekodb_client import Client, QueryBuilder\n\nasync def main():\n    client = Client.new(\"http://localhost:8080\", \"your-api-key\")\n\n    # Simple query with operators\n    query = QueryBuilder() \\\n        .eq(\"status\", \"active\") \\\n        .gte(\"age\", 18) \\\n        .lt(\"age\", 65) \\\n        .limit(10) \\\n        .build()\n\n    results = await client.find(\"users\", query)\n\n    # Complex query with sorting and pagination\n    query = QueryBuilder() \\\n        .in_array(\"status\", [\"active\", \"pending\"]) \\\n        .contains(\"email\", \"@example.com\") \\\n        .sort_desc(\"created_at\") \\\n        .skip(20) \\\n        .limit(10) \\\n        .build()\n\n    results = await client.find(\"users\", query)\n```\n\n### Search Operations\n\n```python\n# Basic text search\nsearch_query = {\n    \"query\": \"programming\",\n    \"min_score\": 0.1,\n    \"limit\": 10\n}\n\nresults = await client.search(\"articles\", search_query)\nfor result in results[\"results\"]:\n    print(f\"Score: {result['score']:.4f} - {result['record']['title']}\")\n\n# Search with field weights\nsearch_query = {\n    \"query\": \"rust database\",\n    \"fields\": [\"title\", \"description\"],\n    \"weights\": {\"title\": 2.0},\n    \"limit\": 5\n}\n\nresults = await client.search(\"articles\", search_query)\n```\n\n### Schema Management\n\n```python\n# Create a collection with schema\nschema = {\n    \"fields\": {\n        \"name\": {\n            \"field_type\": \"String\",\n            \"required\": True,\n            \"regex\": \"^[a-zA-Z ]+$\"\n        },\n        \"email\": {\n            \"field_type\": \"String\",\n            \"required\": True,\n            \"unique\": True\n        },\n        \"age\": {\n            \"field_type\": \"Integer\",\n            \"min\": 0,\n            \"max\": 150\n        }\n    }\n}\n\nawait client.create_collection(\"users\", schema)\n\n# Get collection schema\nschema = await client.get_schema(\"users\")\n```\n\n### Join Operations\n\n```python\n# Single collection join\nquery = {\n    \"join\": {\n        \"collections\": [\"departments\"],\n        \"local_field\": \"department_id\",\n        \"foreign_field\": \"id\",\n        \"as_field\": \"department\"\n    },\n    \"limit\": 10\n}\n\nresults = await client.find(\"users\", query)\n\n# Multi-collection join\nquery = {\n    \"join\": [\n        {\n            \"collections\": [\"departments\"],\n            \"local_field\": \"department_id\",\n            \"foreign_field\": \"id\",\n            \"as_field\": \"department\"\n        },\n        {\n            \"collections\": [\"profiles\"],\n            \"local_field\": \"id\",\n            \"foreign_field\": \"id\",\n            \"as_field\": \"profile\"\n        }\n    ],\n    \"limit\": 10\n}\n\nresults = await client.find(\"users\", query)\n```\n\n## API Reference\n\n### Client\n\n#### `Client.new(base_url: str, api_key: str, should_retry: bool = True, max_retries: int = 3, timeout_secs: int = 30) -> Client`\n\nCreate a new ekoDB client.\n\n**Parameters:**\n\n- `base_url`: The base URL of the ekoDB server\n- `api_key`: Your API key\n- `should_retry`: Enable automatic retries (default: True)\n- `max_retries`: Maximum number of retry attempts (default: 3)\n- `timeout_secs`: Request timeout in seconds (default: 30)\n\n**Returns:**\n\n- A new `Client` instance\n\n### RateLimitInfo\n\nRate limit information is automatically tracked and logged by the client. The\nclient will automatically retry on rate limit errors using the server's\n`Retry-After` header.\n\n#### Properties\n\n- `limit: int` - Maximum requests allowed per window\n- `remaining: int` - Requests remaining in current window\n- `reset: int` - Unix timestamp when the rate limit resets\n\n#### Methods\n\n- `is_near_limit() -> bool` - Check if approaching rate limit (<10% remaining)\n- `is_exceeded() -> bool` - Check if the rate limit has been exceeded\n- `remaining_percentage() -> float` - Get the percentage of requests remaining\n\n### RateLimitError\n\nException raised when rate limit is exceeded (if retries are disabled or\nexhausted).\n\n#### Properties\n\n- `retry_after_secs: int` - Number of seconds to wait before retrying\n\n#### `await client.insert(collection: str, record: dict) -> dict`\n\nInsert a document into a collection.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `record`: A dictionary representing the document\n\n**Returns:**\n\n- The inserted document with ID\n\n#### `await client.find_by_id(collection: str, id: str) -> dict`\n\nFind a document by ID.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `id`: The document ID\n\n**Returns:**\n\n- The found document\n\n#### `await client.find(collection: str, limit: Optional[int] = None) -> List[dict]`\n\nFind documents in a collection.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `limit`: Optional limit on number of results\n\n**Returns:**\n\n- List of matching documents\n\n#### `await client.update(collection: str, id: str, updates: dict) -> dict`\n\nUpdate a document.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `id`: The document ID\n- `updates`: Dictionary of fields to update\n\n**Returns:**\n\n- The updated document\n\n#### `await client.delete(collection: str, id: str) -> None`\n\nDelete a document.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `id`: The document ID\n\n#### `await client.list_collections() -> List[str]`\n\nList all collections.\n\n**Returns:**\n\n- List of collection names\n\n#### `await client.delete_collection(collection: str) -> None`\n\nDelete a collection.\n\n**Parameters:**\n\n- `collection`: The collection name to delete\n\n#### `await client.search(collection: str, query: dict) -> dict`\n\nPerform full-text search on a collection.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `query`: Search query dictionary with fields like `query`, `fields`,\n  `weights`, `min_score`, `limit`\n\n**Returns:**\n\n- Search results with scores and matched records\n\n#### `await client.create_collection(collection: str, schema: dict) -> None`\n\nCreate a collection with a schema.\n\n**Parameters:**\n\n- `collection`: The collection name\n- `schema`: Schema definition dictionary\n\n#### `await client.get_schema(collection: str) -> dict`\n\nGet the schema for a collection.\n\n**Parameters:**\n\n- `collection`: The collection name\n\n**Returns:**\n\n- Schema definition dictionary\n\n#### `await client.get_collection(collection: str) -> dict`\n\nGet collection metadata including schema.\n\n**Parameters:**\n\n- `collection`: The collection name\n\n**Returns:**\n\n- Collection metadata dictionary\n\n## Examples\n\nSee the\n[examples directory](https://github.com/ekoDB/ekodb-client/tree/main/examples/python)\nfor complete working examples:\n\n- `client_simple_crud.py` - Basic CRUD operations\n- `client_query_builder.py` - Complex queries with QueryBuilder\n- `client_search.py` - Full-text search operations\n- `client_schema.py` - Schema management\n- `client_joins.py` - Join operations\n- `client_batch_operations.py` - Batch operations\n- `client_kv_operations.py` - Key-value operations\n- And more...\n\n## Development\n\n### Building\n\n```bash\n# Install maturin\npip install maturin\n\n# Build and install in development mode\nmaturin develop\n\n# Build release wheel\nmaturin build --release\n```\n\n### Testing\n\n```bash\n# Run Python tests\npytest\n\n# Run with coverage\npytest --cov=ekodb\n```\n\n## License\n\nMIT OR Apache-2.0\n\n## Links\n\n- [GitHub](https://github.com/ekoDB/ekodb-client)\n- [Examples](https://github.com/ekoDB/ekodb-client/tree/main/examples/python)\n- [PyPI Package](https://pypi.org/project/ekodb-client/)\n\n",
    "bugtrack_url": null,
    "license": "MIT OR Apache-2.0",
    "summary": "Python client for ekoDB - a high-performance document database",
    "version": "0.3.0",
    "project_urls": {
        "Documentation": "https://github.com/ekoDB/ekodb/tree/main/documentation",
        "Homepage": "https://github.com/ekoDB/ekodb",
        "Repository": "https://github.com/ekoDB/ekodb"
    },
    "split_keywords": [
        "database",
        " ekodb",
        " document-database",
        " nosql"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "fda0b9d0a401b4cd787cc18d34be198b929d908fca1ee201d37b9fc13dee4612",
                "md5": "8eacd5016ff28da92cbdda182e5d0a29",
                "sha256": "9bf7c96640b340c69162fe9d476ba6d877ebc2ca00402354787056e7b424b3dd"
            },
            "downloads": -1,
            "filename": "ekodb_client-0.3.0-cp38-abi3-macosx_11_0_arm64.whl",
            "has_sig": false,
            "md5_digest": "8eacd5016ff28da92cbdda182e5d0a29",
            "packagetype": "bdist_wheel",
            "python_version": "cp38",
            "requires_python": ">=3.8",
            "size": 3334515,
            "upload_time": "2025-11-05T06:11:32",
            "upload_time_iso_8601": "2025-11-05T06:11:32.578720Z",
            "url": "https://files.pythonhosted.org/packages/fd/a0/b9d0a401b4cd787cc18d34be198b929d908fca1ee201d37b9fc13dee4612/ekodb_client-0.3.0-cp38-abi3-macosx_11_0_arm64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "8d586c212cb17596ba826c1f49489ec6e399afac2636b7f846f67701ace1627c",
                "md5": "e5b51edc42bb2fc8896d49d31a64d8a1",
                "sha256": "dcba928bcaf95abbe3941e236115ff4a703c9ff531e06b3feef641504288fe2a"
            },
            "downloads": -1,
            "filename": "ekodb_client-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
            "has_sig": false,
            "md5_digest": "e5b51edc42bb2fc8896d49d31a64d8a1",
            "packagetype": "bdist_wheel",
            "python_version": "cp38",
            "requires_python": ">=3.8",
            "size": 3689985,
            "upload_time": "2025-11-05T06:11:34",
            "upload_time_iso_8601": "2025-11-05T06:11:34.330054Z",
            "url": "https://files.pythonhosted.org/packages/8d/58/6c212cb17596ba826c1f49489ec6e399afac2636b7f846f67701ace1627c/ekodb_client-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "85278993556edbb5a65c2563fef5449ce8696955584277d38b1efb32f54aee3f",
                "md5": "f0ee4113a9aee467b25a304a799f87a1",
                "sha256": "6d59176b8aa833f9969f213337efe5a9a1585a4db4f8ce923db7b48a2078a56a"
            },
            "downloads": -1,
            "filename": "ekodb_client-0.3.0.tar.gz",
            "has_sig": false,
            "md5_digest": "f0ee4113a9aee467b25a304a799f87a1",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 72073,
            "upload_time": "2025-11-05T06:11:35",
            "upload_time_iso_8601": "2025-11-05T06:11:35.915899Z",
            "url": "https://files.pythonhosted.org/packages/85/27/8993556edbb5a65c2563fef5449ce8696955584277d38b1efb32f54aee3f/ekodb_client-0.3.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-11-05 06:11:35",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "ekoDB",
    "github_project": "ekodb",
    "github_not_found": true,
    "lcname": "ekodb-client"
}
        
Elapsed time: 1.03954s