Skip to main content

A lightweight relational algebra CLI over JSONL

Project description

ja - JSONL Algebra

ja is a lightweight command-line tool and Python library for performing relational algebra operations on JSONL (JSON Lines) data. It's designed to be a simple, dependency-free alternative for common data manipulation tasks, inspired by tools like jq and traditional SQL.

Features

  • Perform common relational algebra operations: select, project, join, union, intersection, difference, distinct, sort, product, and group by with aggregations.

  • groupby: A powerful feature that allows you to group data by one or more keys and perform various aggregations on the grouped data.

    • By default, includes sum, avg, min, max, count, list (collect all values), first (first value in group), last (last value in group) aggregations.
    • Can be extended with custom aggregation functions. See "Extending Group By Aggregations" section.
  • Works with JSONL files or piped data from stdin/stdout.

  • Can be used as a CLI tool or as a Python library.

  • No external dependencies.

Installation

There are two main ways to install ja:

For users (from PyPI):

You can install the package directly from PyPI (Python Package Index) using pip. We'll assume the package is published under the name jsonl-algebra (as ja is likely taken):

pip install jsonl-algebra

For developers (from local repository):**

If you have cloned this repository and want to install it for development or from local sources:

pip install .

To install in editable mode for development:

pip install -e .

CLI Usage

The ja command-line tool provides several subcommands for different operations.

General Syntax: ja <command> [options] [file(s)]

If file is omitted for commands that expect a single input, ja reads from stdin.

Examples

  • Select rows where 'amount' is greater than 100:

    cat data.jsonl | ja select 'amount > 100'
    
    ja select 'amount > 100' data.jsonl
    
  • Project 'id' and 'name' columns:

    cat data.jsonl | ja project id,name
    
  • Join two files on a common key:

    ja join users.jsonl orders.jsonl --on user_id=customer_id
    
  • Group by 'category' and count items:

    cat products.jsonl | ja groupby category --agg count
    
  • Group by 'category', count items, and list all product names:

    cat products.jsonl | ja groupby category --agg count --agg list:name\
    

    This will produce output like: {"category": "electronics", "count": 5, "list_name": ["laptop", "mouse", ...]}

  • Group by 'user_id' and get the first action:

    cat user_actions.jsonl | ja groupby user_id --agg first:action
    
  • Sort data by 'timestamp':

    cat logs.jsonl | ja sort timestamp
    

Available Commands

  • select: Filter rows based on a Python expression.
  • project: Select specific columns.
  • join: Join two relations on specified keys.
  • rename: Rename columns.
  • union: Combine two relations (all rows).
  • difference: Rows in the first relation but not the second.
  • distinct: Remove duplicate rows.
  • intersection: Rows common to both relations.
  • sort (maps to sort_by): Sort a relation by specified keys.
  • product: Cartesian product of two relations.
  • groupby (maps to groupby_agg): Group rows by a key and perform aggregations.

Use ja <command> --help for more details on specific commands.

Programmatic API Usage

You can also use ja as a Python library:

import ja
from ja import Row, Relation # For type hinting if needed

# Load data from JSONL files
# users_data = ja.read_jsonl("users.jsonl")
# orders_data = ja.read_jsonl("orders.jsonl")

# Example data (replace with ja.read_jsonl for actual files)
users_data: Relation = [
    {"user_id": 1, "name": "Alice", "status": "active", "email": "alice@example.com"},
    {"user_id": 2, "name": "Bob", "status": "inactive", "email": "bob@example.com"},
    {"user_id": 1, "name": "Alice", "status": "active", "email": "alice@example.com"} # Duplicate for distinct example
]
orders_data: Relation = [
    {"order_id": 101, "customer_id": 1, "item": "Book", "quantity": 1},
    {"order_id": 102, "customer_id": 2, "item": "Pen", "quantity": 5},
    {"order_id": 103, "customer_id": 1, "item": "Notebook", "quantity": 2},
    {"order_id": 104, "customer_id": 1, "item": "Book", "quantity": 3}
]


# Example: Select active users
active_users = ja.select(users_data, lambda row: row.get("status") == "active")
# active_users will be:
# [{'user_id': 1, 'name': 'Alice', 'status': 'active', 'email': 'alice@example.com'},
#  {'user_id': 1, 'name': 'Alice', 'status': 'active', 'email': 'alice@example.com'}]

# Example: Project name and email from distinct active users
distinct_active_users = ja.distinct(active_users)
user_info = ja.project(distinct_active_users, ["name", "email"])
# user_info will be:
# [{'name': 'Alice', 'email': 'alice@example.com'}]

# Example: Join distinct active users with their orders
# Ensure the join key 'user_id' is present in the projected active_users relation
active_users_with_id = ja.project(distinct_active_users, ["user_id", "name", "email"])
joined_data = ja.join(active_users_with_id, orders_data, on=[("user_id", "customer_id")])
# joined_data will be:
# [{'user_id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'order_id': 101, 'item': 'Book', 'quantity': 1},
#  {'user_id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'order_id': 103, 'item': 'Notebook', 'quantity': 2},
#  {'user_id': 1, 'name': 'Alice', 'email': 'alice@example.com', 'order_id': 104, 'item': 'Book', 'quantity': 3}]

# Example: Group joined data by user and sum quantities, list items
grouped_orders = ja.groupby_agg(
    joined_data,
    group_by_key="user_id",
    aggregations=[
        ("sum", "quantity"),
        ("list", "item"),
        ("count", "") # Count groups
    ]
)
# grouped_orders might be (depending on Alice's orders):
# [{'user_id': 1, 'sum_quantity': 6, 'list_item': ['Book', 'Notebook', 'Book'], 'count': 3}]


# Print results (example)
for row in grouped_orders:
    print(row)

# Available functions mirror the CLI commands:
# ja.select, ja.project, ja.join, ja.rename, ja.union,
# ja.difference, ja.distinct, ja.intersection, ja.sort_by,
# ja.product, ja.groupby_agg

Extending Group By Aggregations

By default, ja supports several built-in aggregation functions for the groupby_agg operation. These include: count, sum, avg, min, max, list, first, and last. Syntax for aggregations: agg_name (for count) or agg_name:column_name (e.g., sum:price, list:product_id).

The groupby_agg functionality is designed to be extensible. The core logic resides in the ja.groupby module, which uses a dispatcher pattern.

Define an aggregation helper function

This function will take the collected data for a group (typically a list of values, or a single value for aggregations like first/last) and return the aggregated result.

For instance, if you want to add a custom aggregation function for calculating the median, you would define a function that takes a list of values and returns the median.

def _my_custom_median_agg(collected_values: list) -> float | None:
    numeric_vals = sorted([v for v in collected_values if isinstance(v, (int, float))])
    if not numeric_vals:
        return None
    n = len(numeric_vals)
    mid = n // 2
    if n % 2 == 0:
        return (numeric_vals[mid - 1] + numeric_vals[mid]) / 2
    else:
        return numeric_vals[mid]

Now, register it (if modifying ja directly or for illustration):

# In ja/groupby.py
AGGREGATION_DISPATCHER = {
    # ...
    "median": _my_custom_median_agg,
}

For programmatic use with your own ja instance or a forked version, you could potentially expose a way to register custom aggregators or pass them directly if the API supported it.

If your aggregation requires a specific way of collecting data during the first pass of groupby_agg (different from how list, first, or last collect data), you would need to modify the data collection logic in ja.groupby.groupby_agg.

This structure allows for significant flexibility. For instance, one could implement a general reduce aggregation that takes Python expressions for an initial value and a step function, operating on the list of values collected for a group.

Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue.

License

This project is licensed under the MIT License.

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

jsonl_algebra-0.8.tar.gz (19.7 kB view details)

Uploaded Source

Built Distribution

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

jsonl_algebra-0.8-py3-none-any.whl (14.9 kB view details)

Uploaded Python 3

File details

Details for the file jsonl_algebra-0.8.tar.gz.

File metadata

  • Download URL: jsonl_algebra-0.8.tar.gz
  • Upload date:
  • Size: 19.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.3

File hashes

Hashes for jsonl_algebra-0.8.tar.gz
Algorithm Hash digest
SHA256 7b2f267807713f7f6c50683499aee599a31d96c7e62e75ef4891f49e35dca346
MD5 dc913ca154a15009052d62f3d4324580
BLAKE2b-256 19f2e3fcfc22b37c9acd2ce45aaa606e0f52157a9ffe6df4bd8dcac37dd3b6ed

See more details on using hashes here.

File details

Details for the file jsonl_algebra-0.8-py3-none-any.whl.

File metadata

  • Download URL: jsonl_algebra-0.8-py3-none-any.whl
  • Upload date:
  • Size: 14.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.3

File hashes

Hashes for jsonl_algebra-0.8-py3-none-any.whl
Algorithm Hash digest
SHA256 7083f3ffc72289ec674ac8d4f63ea2e2db1f8556cf1f3cb3480b225caef1c876
MD5 f01191e600c18cd56717e512da2727ca
BLAKE2b-256 e9af95605fdba34c727c7c1f7ea8106eafa639d241be344c41bda958a4e8f5cc

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