Skip to main content

Bulk operations module for Bazis framework.

Project description

Bazis Bulk

PyPI version Python Versions License

An extension package for Bazis, providing batch request processing with transaction support and asynchronous execution.

Quick Start

# Install package
uv add bazis-bulk

# Register route
# router.py
from bazis.core.routing import BazisRouter

router = BazisRouter(prefix='/api/v1')
router.register('bazis.contrib.bulk.router')

# Usage example
curl -X POST http://localhost/api/v1/bulk/ \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "endpoint": "/api/v1/entity/parent_entity/",
      "method": "POST",
      "body": {
        "data": {
          "type": "entity.parent_entity",
          "bs:action": "add",
          "attributes": {"name": "Parent 1"}
        }
      }
    },
    {
      "endpoint": "/api/v1/entity/child_entity/",
      "method": "POST",
      "body": {
        "data": {
          "type": "entity.child_entity",
          "bs:action": "add",
          "attributes": {"child_name": "Child 1"}
        }
      }
    }
  ]'

Table of Contents

Description

Bazis Bulk is an extension package for the Bazis framework that allows executing multiple API requests in a single HTTP request. The package includes:

  • Batch request execution — send multiple operations in one request
  • Transactional mode — all operations execute within a single transaction (atomicity)
  • Non-transactional mode — operations execute independently
  • Support for all HTTP methods — GET, POST, PATCH, PUT, DELETE
  • JSON:API support — work with included resources and relationships
  • Dedicated thread for transactions — guaranteed transaction isolation

Typical use cases:

  • Creating related entities in one request
  • Bulk record updates
  • Atomic operations on multiple resources
  • Reducing HTTP request count (lower latency)

This package requires the base bazis package to be installed.

Requirements

  • Python: 3.12+
  • bazis: latest version
  • PostgreSQL: 12+

Installation

Using uv (recommended)

uv add bazis-bulk

Using pip

pip install bazis-bulk

Usage

Route Registration

Add the route to your main router.py:

from bazis.core.routing import BazisRouter

router = BazisRouter(prefix='/api/v1')

# Register bulk route
router.register('bazis.contrib.bulk.router')

This creates the endpoint: POST /api/v1/bulk/

Request Format

A bulk request is an array of objects, where each object describes a separate HTTP request.

Single element structure:

{
  "endpoint": string,    // Endpoint path (required)
  "method": string,      // HTTP method: GET, POST, PATCH, PUT, DELETE (required)
  "body": object,        // Request body in JSON:API format (optional)
  "headers": array       // Additional headers (optional, currently ignored)
}

Example:

[
  {
    "endpoint": "/api/v1/entity/parent_entity/",
    "method": "POST",
    "body": {
      "data": {
        "type": "entity.parent_entity",
        "bs:action": "add",
        "attributes": {
          "name": "New Parent"
        }
      }
    }
  }
]

Request Parameters

is_atomic (query parameter)

Defines the request execution mode:

  • is_atomic=true (default) — transactional mode

    • All operations execute within a single transaction
    • Any operation error rolls back the entire transaction
    • Response status: 400 if errors occur
  • is_atomic=falsenon-transactional mode

    • Operations execute independently
    • Error in one operation doesn't affect others
    • Response status: 200 even with errors in individual operations

Examples:

# Transactional mode (default)
POST /api/v1/bulk/
POST /api/v1/bulk/?is_atomic=true

# Non-transactional mode
POST /api/v1/bulk/?is_atomic=false

Response Format

Each response item contains the original endpoint, HTTP status, headers, and the parsed body.

Single element structure:

{
  "endpoint": string,    // Endpoint path (required)
  "status": number,      // HTTP status code (required)
  "headers": array,      // ASGI response headers as [name, value] pairs
  "response": object     // Parsed JSON for JSON responses, raw body otherwise (may be null)
}

Headers are returned as emitted by the ASGI app (typically byte pairs).

Transactional Mode

In transactional mode, all operations execute in a dedicated thread with a single database transaction.

Features:

  • All operations either succeed completely or rollback entirely
  • Any operation error causes transaction rollback
  • Overall response status: 400 if errors occur

Request example:

POST /api/v1/bulk/?is_atomic=true
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

[
  {
    "endpoint": "/api/v1/orders/order/",
    "method": "POST",
    "body": {
      "data": {
        "type": "myapp.order",
        "bs:action": "add",
        "attributes": {
          "description": "Order 1",
          "amount": 1000
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/orders/order/",
    "method": "POST",
    "body": {
      "data": {
        "type": "myapp.order",
        "bs:action": "add",
        "attributes": {
          "description": "Order 2",
          "amount": 2000
        }
      }
    }
  }
]

Success response (status 200):

[
  {
    "endpoint": "/api/v1/orders/order/",
    "status": 201,
    "headers": [
      ["content-type", "application/vnd.api+json"]
    ],
    "response": {
      "data": {
        "type": "myapp.order",
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "attributes": {
          "description": "Order 1",
          "amount": 1000
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/orders/order/",
    "status": 201,
    "response": {
      "data": {
        "type": "myapp.order",
        "id": "987e6543-e21b-32d1-b654-426614174001",
        "attributes": {
          "description": "Order 2",
          "amount": 2000
        }
      }
    }
  }
]

Error response (status 400, all operations rolled back):

[
  {
    "endpoint": "/api/v1/orders/order/",
    "status": 201,
    "response": {
      "data": {
        "type": "myapp.order",
        "id": "123e4567-e89b-12d3-a456-426614174000",
        "attributes": {
          "description": "Order 1"
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/orders/order/",
    "status": 403,
    "response": {
      "errors": [
        {
          "status": 403,
          "detail": "Permission denied"
        }
      ]
    }
  }
]

Non-transactional Mode

In non-transactional mode, each operation executes independently in a thread pool.

Features:

  • Operations execute independently
  • Error in one operation doesn't affect others
  • Overall response status: always 200

Request example:

POST /api/v1/bulk/?is_atomic=false
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN

[
  {
    "endpoint": "/api/v1/orders/order/",
    "method": "POST",
    "body": {
      "data": {
        "type": "myapp.order",
        "bs:action": "add",
        "attributes": {"description": "Order 1"}
      }
    }
  },
  {
    "endpoint": "/api/v1/orders/order/999/",
    "method": "DELETE",
    "body": {}
  }
]

Response (status 200, even with errors):

[
  {
    "endpoint": "/api/v1/orders/order/",
    "status": 201,
    "response": {
      "data": {
        "type": "myapp.order",
        "id": "123e4567-e89b-12d3-a456-426614174000"
      }
    }
  },
  {
    "endpoint": "/api/v1/orders/order/999/",
    "status": 404,
    "response": {
      "errors": [
        {
          "status": 404,
          "detail": "Not found"
        }
      ]
    }
  }
]

Examples

Example 1: Creating Related Entities

Creating a parent entity and two child entities in one transaction:

[
  {
    "endpoint": "/api/v1/entity/parent_entity/",
    "method": "POST",
    "body": {
      "data": {
        "type": "entity.parent_entity",
        "bs:action": "add",
        "attributes": {
          "name": "Parent Entity"
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/entity/child_entity/",
    "method": "POST",
    "body": {
      "data": {
        "type": "entity.child_entity",
        "bs:action": "add",
        "attributes": {
          "child_name": "Child 1"
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/entity/child_entity/",
    "method": "POST",
    "body": {
      "data": {
        "type": "entity.child_entity",
        "bs:action": "add",
        "attributes": {
          "child_name": "Child 2"
        }
      }
    }
  }
]

Example 2: Update with Included Resources

Updating a parent entity and its related children:

[
  {
    "endpoint": "/api/v1/entity/parent_entity/123/?include=extended_entity,dependent_entities",
    "method": "PATCH",
    "body": {
      "data": {
        "id": "123",
        "type": "entity.parent_entity",
        "bs:action": "change",
        "attributes": {
          "name": "Updated Parent"
        }
      },
      "included": [
        {
          "id": "456",
          "type": "entity.extended_entity",
          "bs:action": "change",
          "attributes": {
            "extended_name": "Updated Extended"
          }
        },
        {
          "type": "entity.dependent_entity",
          "bs:action": "add",
          "attributes": {
            "dependent_name": "New Dependent"
          },
          "relationships": {
            "parent_entity": {
              "data": {
                "id": "123",
                "type": "entity.parent_entity"
              }
            }
          }
        }
      ]
    }
  }
]

Example 3: Bulk Update

Updating multiple records simultaneously:

[
  {
    "endpoint": "/api/v1/entity/child_entity/child-1/",
    "method": "PATCH",
    "body": {
      "data": {
        "id": "child-1",
        "type": "entity.child_entity",
        "bs:action": "change",
        "attributes": {
          "child_name": "Updated Child 1"
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/entity/child_entity/child-2/",
    "method": "PATCH",
    "body": {
      "data": {
        "id": "child-2",
        "type": "entity.child_entity",
        "bs:action": "change",
        "attributes": {
          "child_name": "Updated Child 2"
        }
      }
    }
  },
  {
    "endpoint": "/api/v1/entity/child_entity/child-3/",
    "method": "PATCH",
    "body": {
      "data": {
        "id": "child-3",
        "type": "entity.child_entity",
        "bs:action": "change",
        "attributes": {
          "child_name": "Updated Child 3"
        }
      }
    }
  }
]

Example 4: Mixed Operations

Create, update, and delete in one request:

[
  {
    "endpoint": "/api/v1/entity/parent_entity/",
    "method": "POST",
    "body": {
      "data": {
        "type": "entity.parent_entity",
        "bs:action": "add",
        "attributes": {"name": "New Parent"}
      }
    }
  },
  {
    "endpoint": "/api/v1/entity/parent_entity/existing-id/",
    "method": "PATCH",
    "body": {
      "data": {
        "id": "existing-id",
        "type": "entity.parent_entity",
        "bs:action": "change",
        "attributes": {"price": "845.42"}
      }
    }
  },
  {
    "endpoint": "/api/v1/entity/child_entity/old-id/",
    "method": "DELETE",
    "body": {}
  }
]

Example 5: JavaScript Client

class BulkClient {
  constructor(apiUrl, token) {
    this.apiUrl = apiUrl;
    this.token = token;
  }

  async executeBulk(operations, isAtomic = true) {
    const url = `${this.apiUrl}/bulk/?is_atomic=${isAtomic}`;

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(operations)
    });

    if (!response.ok) {
      throw new Error(`Bulk request failed: ${response.status}`);
    }

    return await response.json();
  }

  async createMultiple(entities, isAtomic = true) {
    const operations = entities.map(entity => ({
      endpoint: entity.endpoint,
      method: 'POST',
      body: {
        data: {
          type: entity.type,
          'bs:action': 'add',
          attributes: entity.attributes,
          relationships: entity.relationships
        }
      }
    }));

    return await this.executeBulk(operations, isAtomic);
  }

  async updateMultiple(updates, isAtomic = true) {
    const operations = updates.map(update => ({
      endpoint: `${update.endpoint}/${update.id}/`,
      method: 'PATCH',
      body: {
        data: {
          id: update.id,
          type: update.type,
          'bs:action': 'change',
          attributes: update.attributes
        }
      }
    }));

    return await this.executeBulk(operations, isAtomic);
  }
}

// Usage
const bulk = new BulkClient('http://api.example.com/api/v1', jwtToken);

// Create multiple entities atomically
const results = await bulk.createMultiple([
  {
    endpoint: '/api/v1/orders/order',
    type: 'myapp.order',
    attributes: { description: 'Order 1', amount: 1000 }
  },
  {
    endpoint: '/api/v1/orders/order',
    type: 'myapp.order',
    attributes: { description: 'Order 2', amount: 2000 }
  }
], true);

console.log('Created:', results);

// Bulk update without transaction
await bulk.updateMultiple([
  {
    endpoint: '/api/v1/orders/order',
    id: 'order-1',
    type: 'myapp.order',
    attributes: { status: 'completed' }
  },
  {
    endpoint: '/api/v1/orders/order',
    id: 'order-2',
    type: 'myapp.order',
    attributes: { status: 'completed' }
  }
], false);

Example 6: Python Client

import requests
from typing import List, Dict, Any

class BulkClient:
    def __init__(self, api_url: str, token: str):
        self.api_url = api_url
        self.token = token
        self.headers = {
            'Authorization': f'Bearer {token}',
            'Content-Type': 'application/json'
        }

    def execute_bulk(
        self,
        operations: List[Dict[str, Any]],
        is_atomic: bool = True
    ) -> List[Dict[str, Any]]:
        """Execute bulk request"""
        url = f"{self.api_url}/bulk/?is_atomic={str(is_atomic).lower()}"

        response = requests.post(
            url,
            headers=self.headers,
            json=operations
        )
        response.raise_for_status()

        return response.json()

    def create_with_related(
        self,
        parent_data: Dict[str, Any],
        children_data: List[Dict[str, Any]]
    ) -> List[Dict[str, Any]]:
        """Create parent entity with children in one transaction"""
        operations = [
            {
                'endpoint': parent_data['endpoint'],
                'method': 'POST',
                'body': {
                    'data': {
                        'type': parent_data['type'],
                        'bs:action': 'add',
                        'attributes': parent_data['attributes']
                    }
                }
            }
        ]

        for child in children_data:
            operations.append({
                'endpoint': child['endpoint'],
                'method': 'POST',
                'body': {
                    'data': {
                        'type': child['type'],
                        'bs:action': 'add',
                        'attributes': child['attributes'],
                        'relationships': child.get('relationships', {})
                    }
                }
            })

        return self.execute_bulk(operations, is_atomic=True)

# Usage
bulk = BulkClient('http://api.example.com/api/v1', jwt_token)

# Create order with items
results = bulk.create_with_related(
    parent_data={
        'endpoint': '/api/v1/orders/order',
        'type': 'myapp.order',
        'attributes': {
            'description': 'New Order',
            'customer': 'John Doe'
        }
    },
    children_data=[
        {
            'endpoint': '/api/v1/orders/orderitem',
            'type': 'myapp.orderitem',
            'attributes': {
                'product': 'Product 1',
                'quantity': 2,
                'price': '100.00'
            }
        },
        {
            'endpoint': '/api/v1/orders/orderitem',
            'type': 'myapp.orderitem',
            'attributes': {
                'product': 'Product 2',
                'quantity': 1,
                'price': '50.00'
            }
        }
    ]
)

print(f"Created order with {len(results) - 1} items")

License

Apache License 2.0

See LICENSE file for details.

Links

Support

If you have questions or issues:


Made with ❤️ by the Bazis team

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bazis_bulk-2.2.0.tar.gz (76.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bazis_bulk-2.2.0-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

File details

Details for the file bazis_bulk-2.2.0.tar.gz.

File metadata

  • Download URL: bazis_bulk-2.2.0.tar.gz
  • Upload date:
  • Size: 76.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for bazis_bulk-2.2.0.tar.gz
Algorithm Hash digest
SHA256 372c11455d700fbb3935ce1627a58b2078856fc7c04e5000fd1784fcfe3f6eac
MD5 19d0f0b5d34601b4d8407556966ce2f7
BLAKE2b-256 00860953f2b2c82e0aff6930f8bac6864e1e65132485046857504851bf3aec01

See more details on using hashes here.

File details

Details for the file bazis_bulk-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: bazis_bulk-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 14.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.28 {"installer":{"name":"uv","version":"0.9.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for bazis_bulk-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 96dd22c267cf45da6a5fdaa82e550acc7b85e9f91521d0b05962c3aeb9e4416d
MD5 f1aeba574d42afe0ed91c8756ba47ba4
BLAKE2b-256 ec3c0591b24679adab6aa56ff80e82d91ee03013cb78184c67e6c51986af1fb4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page