json-stream

repository·master·Indexed 41 days ago

https://github.com/daggaz/json-stream

A streaming JSON encoder and decoder for Python (version 2.5.1) that enables memory-efficient processing of large datasets by decoding data incrementally. It features a Rust-powered tokenizer for performance, support for transient and persistent reading modes, a visitor-based parsing pattern, and specialized loaders for urllib, requests, and httpx. It also provides decorators like @streamable_list and @streamable_dict to generate large JSON structures iteratively.

Tokens
8.7K
Snippets
31
Records
39
Agent score
50%

What's inside json-stream

  1. Overview of json-stream features

    master

    json-stream is a simple streaming JSON parser and encoder designed to reduce memory consumption and latency.

    Key Capabilities:

    • Streaming Reading: Decode JSON from files, URLs, or iterators using a pythonic dict/list-like interface or a visitor-based interface. It can process multiple JSON documents in a single stream and handle JSON mixed with non-JSON data.
    • Streaming Writing: Stream JSON objects as they are being generated.
    • Performance: Uses native code parsing speedups (Rust tokenizer) on most common platforms, with a pure Python fallback.
    • Memory Efficiency: Only requires enough memory to hold the data currently being parsed.
  2. How to mix Transient and Persistent modes

    master

    You can selectively change the persistence of data as you consume it. This is useful when you want to iterate through a large list transiently but need the individual items within that list to be persistent (allowing random access to their keys).

    From Transient to Persistent

    Call the .persistent() method on a transient list or dict-like object. This makes the children of that object persistent, not the object itself.

    From Persistent to Transient

    Call the .transient() method on a persistent object. This causes the object to produce transient children.

    Note: When using .transient(), accessing data that was already read from the parent persistent object will still work, but accessing data in the new transient child that has already been passed in the stream will raise an exception.

    import json_stream
    
    # Iterate transient list, but produce persistent items
    data = json_stream.load(f) 
    for result in data['results'].persistent():
        # result is now a persistent dict-like object
        print(result['x'])
        print(result['x'])  # This works because 'result' is persistent
  3. Compare json_stream.load() vs standard json.load()

    master

    When reading large JSON documents, json_stream.load() provides two main advantages over the standard json.load():

    1. Memory Efficiency: json.load() reads the entire document into memory as a string and then converts it to Python types. json_stream.load() only buffers enough from the stream to produce the next item, significantly reducing memory overhead.
    2. Reduced Latency: json.load() requires the entire document to be parsed before any data is accessible. json_stream.load() produces data as soon as it becomes available in the stream, allowing you to process items (e.g., the first 10 items in a list of millions) immediately.
  4. Compare json-stream writing vs standard json.dump()

    master

    When writing JSON, json-stream solves the limitations of json.dump():

    1. Memory Efficiency: json.dump() requires the entire document to be constructed as standard Python types (dict, list, etc.) in memory before writing. json-stream allows you to generate data one item at a time, consuming only the memory required for the current item.
    2. Reduced Latency: json.dump() can only start writing once all data is generated. json-stream allows data to be written to the output file as it is produced.
  5. Stream JSON from URLs (urllib, requests, httpx)

    master

    The library provides specialized loaders for common Python HTTP libraries to enable streaming directly from a URL.

    urllib

    Pass the response object directly to json_stream.load().

    requests

    1. You must pass stream=True to the request.
    2. Use json_stream.requests.load(response). Note: This uses a default chunk_size of 10k bytes. For more responsive consumption, you can tune chunk_size to 1 to disable buffering.

    httpx

    1. Use client.stream(...) to initiate the request.
    2. Use json_stream.httpx.load(response). Note: Similar to requests, this uses a 10k byte chunk size by default.

    All these methods also support the visitor pattern via json_stream.requests.visit() and json_stream.httpx.visit().

    # requests example
    import requests
    import json_stream.requests
    
    with requests.get('http://example.com/data.json', stream=True) as response:
        data = json_stream.requests.load(response)
    
    # httpx example
    import httpx
    import json_stream.httpx
    
    with httpx.Client() as client, client.stream('GET', 'http://example.com/data.json') as response:
        data = json_stream.httpx.load(response)
  6. Handle mixed JSON and binary data with Rust tokenizer

    master

    If you are using the json-stream-rs-tokenizer (the default for speed), you can parse files containing JSON followed by binary data by using the correct_cursor=True argument.

    Workflow for JSON then Binary

    1. Call json_stream.load(f, correct_cursor=True) to read the JSON header.
    2. Call header.read_all() to ensure the stream is read up to the end of the JSON.
    3. Call header.tokenizer.park_cursor() to 'park' the underlying file cursor at the correct position.
    4. Proceed to read the binary data from the file.

    Workflow for Binary then JSON

    Simply read the binary data from the file using standard file methods before calling json_stream.load(f).

    Workflow for JSON -> Binary -> JSON

    1. Read the first JSON with correct_cursor=True.
    2. Call data1.read_all() and data1.tokenizer.park_cursor().
    3. Read the binary data.
    4. Call json_stream.load(f) for the second JSON document.
    import json_stream
    
    with open('test.bin', 'rb') as f:
        # 1. Read first JSON
        data1 = json_stream.load(f, correct_cursor=True)
        data1.read_all()
        data1.tokenizer.park_cursor()
    
        # 2. Read binary data
        binary_data = f.read(1024)
    
        # 3. Read second JSON
        data2 = json_stream.load(f)
  7. Use StreamableDict and StreamableList for incremental JSON building

    master

    The StreamableDict and StreamableList classes are designed to work with standard JSON encoders like json.dump. They mimic the behavior of standard dict and list types but pull their content from an underlying iterator.

    • StreamableList: Inherits from list. It is initialized with an iterable and yields items from that iterable as it is consumed.
    • StreamableDict: Inherits from dict. It is initialized with an iterable that must yield key-value pairs. It provides an .items() method that returns itself to facilitate standard dictionary iteration.
  8. Understand the difference between Transient and Persistent JSON streams

    master

    The json-stream library provides two modes of operation for JSON objects and lists:

    1. Transient Streams: These are memory-efficient as they do not cache data. They only allow forward iteration. If you attempt to access a key or index that has already been passed in the stream, a TransientAccessException is raised.
    2. Persistent Streams: These cache data as they are read. Once a part of the JSON is read, it is stored in memory (using OrderedDict for objects or list for lists), allowing you to access previously seen keys or indices multiple times.

    You can switch between these modes using the .transient() and .persistent() methods on the stream objects.

  9. Complete example: Generating nested streamable JSON

    master

    This example demonstrates how to combine @streamable_list and @streamable_dict to create a nested JSON structure where data is written to stdout as it is generated (simulated with time.sleep).

    import sys
    import json
    import time
    from json_stream.writer import streamable_dict, streamable_list
    
    @streamable_list
    def generate_list(n):
        for i in range(n):
            yield i
            time.sleep(1)
    
    @streamable_dict
    def generate_dict(n):
        for i in range(n):
            yield i, i ** 2
            time.sleep(1)
        yield "a list", generate_list(n)
    
    data = generate_dict(5)
    json.dump(data, sys.stdout, indent=2)
  10. Parse multiple JSON documents with `load_many()` and `visit_many()`

    master

    If your data consists of a sequence of top-level JSON values (like NDJSON or concatenated JSON), use these functions:

    • json_stream.load_many(f): Yields each top-level JSON value as it is parsed.
    • json_stream.visit_many(f, visitor): Visits each top-level JSON value using the visitor pattern and yields control after each one.

    Concatenated JSON Boundaries

    json-stream can unambiguously detect boundaries for objects {...}, arrays [...], and literals true, false, null without delimiters. However, numbers and strings require a delimiter (like a space or newline) between consecutive documents to prevent tokenizer errors.

    import json_stream
    
    # Reading NDJSON
    with open("events.ndjson", "rb") as f:
        for item in json_stream.load_many(f):
            handle(item)
    
    # Visiting many documents
    def visitor(value, path):
        ...
    
    for _ in json_stream.visit_many(open("events.ndjson", "rb"), visitor):
        pass
  11. Wrap existing iterables with streamable_list and streamable_dict

    master

    If you already have an existing iterable (like a range or a list), you can wrap it using streamable_list or streamable_dict from json_stream.writer to make it compatible with json.dump() for streaming output.

    import json
    import sys
    from json_stream.writer import streamable_list
    
    data = streamable_list(range(10))
    json.dump(data, sys.stdout)
  12. Encode json-stream objects back to JSON

    master

    To re-output (encode) persistent json-stream objects using the standard json.dump() or json.dumps(), you must provide a way for the encoder to handle the streaming types.

    Option 1: Using default argument

    Pass json_stream.encoding.default to the default parameter of json.dumps().

    Option 2: Using JSONStreamEncoder class

    Pass json_stream.encoding.JSONStreamEncoder to the cls parameter of json.dumps().

    Option 3: Using a Context Manager

    If you are using a library that calls json.dumps() internally, you can monkey-patch the encoder for the duration of a with block using JSONStreamEncoder().

    Thread Safety: For multi-threaded environments, use json_stream.dump.threading.ThreadSafeJSONStreamEncoder to ensure the patch is applied correctly across threads.

    import json
    import json_stream
    from json_stream.dump import JSONStreamEncoder, default
    
    # Option 1
    print(json.dumps(data, default=default))
    
    # Option 2
    print(json.dumps(data, cls=JSONStreamEncoder))
    
    # Option 3 (for external libraries)
    with JSONStreamEncoder():
        some_library_function_out_of_your_control(data)