Skip to content

feat(python-driver): add public API for connection pooling and model dict conversion#2374

Open
uesleilima wants to merge 2 commits intoapache:masterfrom
uesleilima:feat/python-driver-public-api-enhancements
Open

feat(python-driver): add public API for connection pooling and model dict conversion#2374
uesleilima wants to merge 2 commits intoapache:masterfrom
uesleilima:feat/python-driver-public-api-enhancements

Conversation

@uesleilima
Copy link
Copy Markdown

Summary

Two enhancements to the Python driver's public API:

1. Public API for connection pooling / external connection management (#2369)

The Age class creates and owns a single connection internally. There's no supported way to register agtype adapters on externally-managed connections (e.g., from psycopg_pool.ConnectionPool).

Solution: Add configure_connection(conn, graph_name=None, skip_load=False) that:

  • Sets search_path to ag_catalog
  • Fetches agtype OIDs and registers AgeLoader
  • Optionally checks/creates the graph
  • Does not call LOAD 'age' by default (suitable for managed PostgreSQL services)

Also explicitly exports AgeLoader, ClientCursor, and configure_connection as public symbols in age/__init__.py, eliminating the need for # type: ignore when accessing driver internals.

Usage with connection pool:

from age import configure_connection, ClientCursor
from psycopg_pool import ConnectionPool

pool = ConnectionPool(
    conninfo="host=localhost dbname=mydb",
    configure=configure_connection,
    kwargs={"cursor_factory": ClientCursor},
)

2. Vertex/Edge/Path model classes dict/JSON conversion (#2371)

The driver's model classes lack conversion to plain Python data structures, forcing every consumer to write custom normalization.

Solution: Add to_dict() methods:

  • Vertex.to_dict(){"id", "label", "properties"}
  • Edge.to_dict(){"id", "label", "start_id", "end_id", "properties"}
  • Path.to_dict() → list of to_dict() results

All return plain dicts/lists that work directly with json.dumps().

Tests

Added 8 new unit tests (no DB required):

  • TestModelToDict (5 tests): vertex, edge, path to_dict(), empty properties, JSON serializability
  • TestPublicImports (3 tests): verify configure_connection, AgeLoader, ClientCursor are importable

Closes

…dict conversion

Add two enhancements to the Python driver's public API:

1. Add configure_connection() function that registers AGE agtype adapters
   on an existing psycopg connection without creating a new one. This
   enables use with external connection pools (e.g. psycopg_pool) and
   managed PostgreSQL services where LOAD 'age' may be restricted.
   Also explicitly export AgeLoader and ClientCursor as public symbols
   in age/__init__.py. (apache#2369)

2. Add to_dict() methods to Vertex, Edge, and Path model classes for
   conversion to plain Python dicts. This enables direct JSON
   serialization with json.dumps() without requiring custom conversion
   logic. (apache#2371)

   - Vertex.to_dict() returns {id, label, properties}
   - Edge.to_dict() returns {id, label, start_id, end_id, properties}
   - Path.to_dict() returns a list of to_dict() results

Closes apache#2369
Closes apache#2371
…plugins

- Replace confusing `skip_load` (double-negative) with `load` (positive
  boolean, default False). The default now correctly matches the intent:
  no LOAD by default for connection pool / managed PostgreSQL use cases.
- Add `load_from_plugins` parameter for parity with setUpAge().
- Fix docstring to accurately describe parameter behavior.
- Add 6 unit tests for configure_connection covering: default no-load,
  explicit load, load_from_plugins, search_path always set, adapter
  registration, and graph_name check delegation.

Made-with: Cursor
@tcolo
Copy link
Copy Markdown

tcolo commented Apr 5, 2026

@uesleilima thanks a lot for your patch. I let Claude code review run over the patch. Can you please address the issues ?

Code Review

Summary

Adds two useful features to the Python driver: a configure_connection() function for externally-managed connection pools, and to_dict() serialization methods on graph model classes. The overall approach is sound and the test coverage is solid. A few correctness and consistency issues are worth addressing before merging.


🔴 Critical Issues

# File Issue
1 age/age.py TypeInfo.fetch() is called outside the with conn.cursor() block. In setUpAge it lives inside the block. If the connection is in autocommit=False mode the SET search_path change may not be visible to TypeInfo.fetch depending on transaction isolation. Move TypeInfo.fetch inside the cursor block (or at least after conn.commit()).
2 age/age.py load_from_plugins=True without load=True is silently ignored. A caller who passes load_from_plugins=True expecting plugins-path loading will get no LOAD at all and no warning. Raise a ValueError or at least emit a warning.

🟡 Suggestions

# File Suggestion Category
1 age/age.py Add type annotations consistent with the rest of the file (conn: psycopg.connection, graph_name: Optional[str] = None, etc.). Maintainability
2 age/age.py Consider adding __all__ to age.py to cleanly control the public surface instead of double-importing in __init__.py. Maintainability
3 age/models.py Path.to_dict() falls back to else e for non-AGObj entities — this may return non-JSON-serializable objects. Add a comment or use str(e) as a safe fallback. Correctness
4 age/models.py dict(self.properties) is a shallow copy. Nested mutable values will still be shared references. Document this or use copy.deepcopy if full JSON safety is the goal. Correctness
5 test_age_py.py Missing test: AgeNotSet is raised when TypeInfo.fetch returns None. This is a key code path in configure_connection with no coverage. Testing
6 test_age_py.py No test asserting that load_from_plugins=True, load=False does not execute any LOAD. Ties directly to the silent-ignore issue above. Testing
7 test_age_py.py Asserting "LOAD" not in str(call_args) is fragile — depends on Python's internal repr. Prefer explicit assert_called_with(...) checks. Testing

✅ What Looks Good

  • The configure_connection docstring is clear and explicitly explains the managed-PostgreSQL use case (Azure, AWS RDS) — genuinely useful.
  • Default load=False is the right choice for a connection-pool scenario and the rationale is well communicated.
  • to_dict() implementations are clean, simple, and consistent across Vertex, Edge, and Path.
  • TestConfigureConnection correctly uses unittest.mock to test without a live database.
  • test_registers_agtype_adapters asserts adapter registration by OID — precise and correct.
  • TestPublicImports is a lightweight but effective guard against accidental API regressions.

Verdict: Request Changes — The silent load_from_plugins ignore and the TypeInfo.fetch cursor-scope issue are real correctness risks worth fixing. The feature direction is right and most of the code is clean.

Copy link
Copy Markdown

@tcolo tcolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check the comment I made

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants