Gel Python Driver

repository·master·Indexed 19 days ago

https://github.com/geldata/gel-python

The official Python driver for Gel, supporting both blocking IO and asyncio. It enables developers to manage schemas and data using raw Gel query strings or programmatic Pydantic models. The driver includes features for managing execution state, configuring transaction and retry options, and providing both Client and AsyncIOClient implementations for executing EdgeQL commands.

Tokens
27.2K
Snippets
100
Records
125
Agent score
64%

What's inside gel-python

  1. How the Query Builder uses Python Descriptors

    master

    The query builder leverages Python descriptors to provide a dual-purpose API. This allows the same attribute name to behave differently depending on whether it is accessed via a class or an instance:

    1. Class-level access: Accessing an attribute on the class (e.g., User.friends) returns a query builder path (AST nodes) used for constructing queries.
    2. Instance-level access: Accessing the same attribute on an instance (e.g., user.friends) returns the actual data loaded from the database.

    This pattern enables intuitive syntax like User.friends.name for building queries.

  2. Manage Client Connection Pools

    master

    Both edgedb.Client and edgedb.AsyncIOClient include built-in connection pools.

    • edgedb.Client: All methods are thread-safe. You can share a single client instance across multiple threads to run queries concurrently.
    • edgedb.AsyncIOClient: Designed to be shared among different asyncio.Task or coroutines for concurrency.

    Connections are created lazily. To explicitly connect to the database (e.g., during application startup), use the ensure_connected() method on the client.

  3. How reachable objects are handled during save and sync

    master

    When you call save() or sync(), the operation is not limited to the specific objects you pass in. Gel traverses the object graph and applies the operation to all reachable objects via links.

    For example, if you call client.save(bar) and bar has a link to foo, both bar and foo will be included in the save plan.

    Note on sync() behavior: For sync(), all reachable objects are refetched. This is necessary because even if an object hasn't changed directly in Python, it might be affected by changes in related objects (e.g., through computed backlinks).

    foo = default.Foo(n=1)
    bar = default.Bar(foo=foo)
    # Both bar and foo will be processed
    client.save(bar)
  4. Understand collection modes: write mode vs read-write mode

    master

    When working with multi-link or multi-property collections in Gel, the collection exists in one of two modes. This determines whether you can inspect the current state of the data or only modify it.

    write mode

    Used when the parent object exists in the database, but its specific collection state (the links or properties) has not been fetched.

    • Allowed: .append() and .remove() operations (which translate to += and -= in EdgeQL).
    • Forbidden: Accessing the collection's state via __iter__, __len__, __contains__, or __bool__. Attempting these will raise an error: "cannot access unfetched data".

    read-write mode

    Used when the collection's state is known (e.g., it was just initialized, explicitly assigned a new list, or fetched from the database).

    • Allowed: All operations, including iteration, checking length, and clearing the collection.
  5. How transactions work in AsyncIOClient

    master

    The transaction() method opens a retryable transaction loop. This is the preferred way to run database transactions because it automatically attempts to re-execute the transaction block if a transient error occurs (like a network error or a serialization error).

    Usage Pattern:

    1. Iterate over the client.transaction() generator.
    2. Use async with tx: to manage the transaction lifecycle.
    3. Crucially, execute all queries on the tx object, not the original client object.

    Note that the transaction starts lazily; a connection is only pulled from the pool when the first query is issued on the transaction instance.

    import edgedb
    
    client = edgedb.create_async_client()
    
    async for tx in client.transaction():
        async with tx:
            # Perform operations on the 'tx' object
            value = await tx.query_single("SELECT Counter.value")
            await tx.execute(
                "UPDATE Counter SET { value := <int64>$value }",
                value=value + 1,
            )
  6. Understand the Gel-Python Model Hierarchy

    master

    The model system uses a layered approach to bridge Gel's database schema with Python's type system and Pydantic's validation. The hierarchy is as follows:

    • GelSourceModel: The base Pydantic wrapper that enables change tracking.
    • GelModel: The primary class used to represent and handle objects.
    • GelLinkModel: Specifically handles link properties (relationships).
    • ProxyModel: A complex wrapper used to route attributes to wrapped objects and handle link properties dynamically.
  7. Manage link properties in Python

    master

    Link properties in Gel follow the Python object model. This means that how you update a link determines whether you are modifying specific properties or replacing the entire link.

    • Updating a property: To update a specific property on a linked object without replacing the link itself, access the property via .__linkprops__.
    • Replacing a link: Assigning a new object to a link field will overwrite the existing link and all its associated link properties.
    foo = default.Foo()
    # bar.foo is initialized with link properties a, b, and c
    bar = default.Bar(foo=default.Bar.foo.link(foo, a=1, b=2, c=3))
    client.sync(bar)
    
    # Scenario 1: Update only property 'a', keeping 'b' and 'c' intact
    bar.foo.__linkprops__.a = 9
    
    # Scenario 2: Reset the entire link (overwrites a, b, and c)
    bar.foo = foo
    foo = default.Foo()
    bar = default.Bar(foo=default.Bar.foo.link(foo, a=1, b=2, c=3))
    client.sync(bar)
    
    # updates a, keeps b and c
    bar.foo.__linkprops__.a = 9
    
    # resets a, b, and c
    bar.foo = foo
  8. Equality and Hashing behavior for Gel objects

    master

    When working with Gel objects, equality and hashing follow these rules:

    • Identity-based equality: Objects with the same ID are considered equal, even if their data differs.
    • New objects: Objects without an ID are only equal to themselves.
    • Link properties: Properties on links are ignored during equality comparisons.
    • Hashability: Objects with IDs are hashable; new objects (without IDs) are not.
  9. Manage Execution State

    master

    The State object defines the execution context for EdgeQL commands, including the default module, module aliases, session configuration, and global values.

    You can modify the state using with_state on a client, which returns a shallow copy of the client with the updated state. Alternatively, the client provides convenience shortcuts for specific state components.

    State Components:

    • Default Module: The module used for commands if no module is specified. Use with_default_module(module) to change it.
    • Module Aliases: Mappings of alias names to target modules. Use with_module_aliases(**aliases) to add/merge or without_module_aliases(*aliases) to remove them.
    • Session Config: Non-system-level configuration settings. Use with_config(**config) to merge or without_config(*config_names) to reset.
    • Global Values: Global variables available in the session. Use with_globals(**globals_) to merge or without_globals(*global_names) to reset.

    Note on Resolution: When setting globals or aliases, names are resolved using the current state's default module and aliases. Changing the module or aliases via with_default_module or with_module_aliases after setting globals will not retroactively change how previously set globals were resolved.

    # Example of using state shortcuts to set a default module and a global value
    new_client = client.with_default_module('my_module').with_globals(my_var=42)
    
    # Using the general with_state method
    new_state = edgedb.State(default_module='custom_mod', globals_={'key': 'value'})
    client_with_state = client.with_state(new_state)
  10. Refetching behavior for links

    master

    When links are refetched during a sync() operation, the target of a link on the source object is updated based on this priority:

    1. The existing link target.
    2. A reachable object (either existing or a newly refetched object). If multiple are available, one is chosen arbitrarily.
    3. A new object that only contains an id.

    To maintain performance, multi-links are not refetched entirely. Instead, Gel reconciles the existing data with a delta (a list of new and updated object IDs) using a filter. The filter includes:

    • All existing link target IDs currently in the Python field.
    • All IDs present in the delta.

    Warning: If you are using partially-fetched multi-links (using filter, offset, or limit), these original criteria may no longer apply accurately after the reconciliation process completes.

  11. How collection updates translate to EdgeQL

    master

    Gel uses specific semantics for updating collections on a model instance. Understanding these ensures you perform the correct operation for your intent:

    • Replacement: model.pointer = [ ... ] always means "replace with new data" (translates to = in EdgeQL).
    • Addition: model.pointer.append(...) always translates to += in EdgeQL.
    • Removal: model.pointer.remove(...) always translates to -= in EdgeQL.
    • Direct Subtraction: model.pointer -= obj translates to -= in EdgeQL.
    • Clearing: model.pointer.clear() behavior depends on the mode:
      • In read-write mode: Resets any local changes made to the collection before save() is called.
      • In write mode: Translates to -= the existing items in the database (effectively removing the fetched/existing links).
  12. Build the EdgeDB driver from source

    master

    To build the driver from a Git checkout, you must satisfy the following requirements:

    1. A working C compiler.
    2. CPython header files (e.g., python3-dev on Debian/Ubuntu or python3-devel on RHEL/Fedora).

    To install in editable mode, run pip install -e . from the root of the source directory.

    If you need a debug build that includes additional runtime checks (at the cost of performance), set the EDGEDB_DEBUG environment variable to 1 during the installation process.

    # Standard editable install
    $ pip install -e .
    
    # Debug build with extra runtime checks
    $ env EDGEDB_DEBUG=1 pip install -e .