BaseProvider
Database provider interface. All persistence adapters (SQLAlchemy, Elasticsearch, etc.) implement this contract.
See Database Adapters for concrete adapter configuration.
Bases: RegisterLookupMixin
Provider implementation for each database.
Acts as a gateway to configure the database, retrieve connections, and perform commits.
Building a Database Adapter
To create a new database adapter, implement these components:
- Provider (extends
BaseProvider) - 12 abstract methods (see below) +
capabilitiesproperty - Manages connections, sessions, and database lifecycle
-
Reference:
protean.adapters.repository.memory.MemoryProvider -
DAO (extends
BaseDAOfromprotean.port.dao) - 8 abstract methods:
_filter,_create,_update,_update_all,_delete,_delete_all,_raw,has_table - Handles data access operations using sessions from the Provider
-
BaseDAOprovides lifecycle wrappers (get,save,create,update,delete) — you implement the underscored internals -
DatabaseModel (extends
BaseDatabaseModelfromprotean.core.database_model) - 2 abstract methods:
from_entity,to_entity - Use
_entity_to_dict()helper for shared field extraction -
construct_database_model_classauto-generates models;decorate_database_model_classwraps user-defined@domain.model -
Lookups (extends
BaseLookupfromprotean.port.dao) - Required lookups: exact, iexact, contains, icontains, startswith,
endswith, gt, gte, lt, lte, in, isnull (see
REQUIRED_LOOKUPS) - Register with
@YourProvider.register_lookup -
Each lookup implements
as_expression()returning adapter-native comparison -
Registration function
- A
register()function that callsregistry.register(name, class_path) - Wrap imports in try/except for optional dependencies
- Add entry point in
pyproject.tomlunder[project.entry-points."protean.providers"]
Session Protocol
get_session() and get_connection() must return objects that
satisfy :class:SessionProtocol (commit, rollback, close,
and an is_active flag).
The BaseDAO's _commit_if_standalone() calls these methods when
operating outside a Unit of Work. Adapters without real transactions
(e.g., Elasticsearch) should provide a session object with no-op
implementations and is_active = True.
Call Flow
Initialization::
Domain.init()
→ ProviderRegistry.get(name) # loads your Provider class
→ Provider.__init__(name, domain, conn_info)
→ Provider._create_database_artifacts() # if setup_database() called
Persist (within UnitOfWork)::
Repository.add(aggregate)
→ DAO.save(aggregate)
→ DAO._validate_and_update_version(aggregate)
→ DatabaseModel.from_entity(aggregate) # your conversion
→ DAO._create(model_obj) or DAO._update(model_obj)
# UoW holds session — no commit yet
UnitOfWork.__exit__()
→ session.commit() # your session
# On error: session.rollback()
Persist (standalone, no UoW)::
Repository.add(aggregate)
→ DAO.save(aggregate)
→ DatabaseModel.from_entity(aggregate)
→ DAO._create(model_obj) or DAO._update(model_obj)
→ DAO._commit_if_standalone(conn)
→ conn.commit() / conn.rollback() / conn.close()
Retrieve::
Repository.get(identifier)
→ DAO.get(identifier)
→ DAO.query.filter(id=identifier).all()
→ DAO._filter(criteria, offset, limit, order_by)
# Must return ResultSet(items, total)
→ DatabaseModel.to_entity(item) # your conversion
→ DAO._sync_event_position(entity)
→ DAO._track_in_uow(entity)
Lifecycle::
Provider._create_database_artifacts() # create tables/indices
Provider._drop_database_artifacts() # drop tables/indices
Provider._data_reset() # truncate all data (tests)
Provider.is_alive() # health check
Provider.close() # release connections
Initialize Provider with Connection/Adapter details
Source code in src/protean/port/provider.py
223 224 225 226 227 228 | |
capabilities
abstractmethod
property
capabilities: DatabaseCapabilities
Return the capabilities of this database provider.
has_capability
has_capability(capability: DatabaseCapabilities) -> bool
Check if provider has a specific capability.
Source code in src/protean/port/provider.py
235 236 237 | |
has_all_capabilities
has_all_capabilities(
capabilities: DatabaseCapabilities,
) -> bool
Check if provider has all the specified capabilities.
Source code in src/protean/port/provider.py
239 240 241 | |
has_any_capability
has_any_capability(
capabilities: DatabaseCapabilities,
) -> bool
Check if provider has any of the specified capabilities.
Source code in src/protean/port/provider.py
243 244 245 | |
validate_lookups
classmethod
validate_lookups() -> list[str]
Check that all required lookups are registered.
Returns a list of missing lookup names. Empty list means all required lookups are present.
Source code in src/protean/port/provider.py
247 248 249 250 251 252 253 254 255 | |
get_session
abstractmethod
get_session() -> SessionProtocol
Establish a new session with the database.
Must return an object satisfying :class:SessionProtocol.
Typically the session factory should be created once per application. Which is then held on to and passed to different transactions.
In Protean's case, the session scope and the transaction scope match. Which means that a new session is created when a transaction needs to be initiated (at the beginning of request handling, for example) and terminated (after committing or rolling back) at the end of the process. The session will be used as a component in Unit of Work Pattern, to handle transactions reliably.
Sessions are made available to requests as part of a Context Manager.
Source code in src/protean/port/provider.py
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | |
get_connection
abstractmethod
get_connection() -> Any
Get the connection object for the repository.
Must return an object satisfying :class:SessionProtocol.
Source code in src/protean/port/provider.py
288 289 290 291 292 293 | |
is_alive
abstractmethod
is_alive() -> bool
Check if the connection is alive
Source code in src/protean/port/provider.py
295 296 297 | |
close
abstractmethod
close() -> None
Close the provider and clean up any persistent connections or resources.
This method should be called to properly dispose of connections and free up resources when the provider is no longer needed. Implementations should: - Close any connection pools - Dispose of any persistent connections - Clean up any other resources (engines, clients, etc.)
Source code in src/protean/port/provider.py
299 300 301 302 303 304 305 306 307 308 | |
get_dao
abstractmethod
get_dao(
entity_cls: type[Any], database_model_cls: type[Any]
) -> Any
Return a DAO object configured with a live connection
Source code in src/protean/port/provider.py
310 311 312 | |
decorate_database_model_class
abstractmethod
decorate_database_model_class(
entity_cls: type[Any], database_model_cls: type[Any]
) -> type[Any]
Enhance a user-defined DatabaseModel class with adapter internals.
Called when the user has defined a custom @domain.model for an
entity. The model class is passed in — add adapter-specific base
classes, column mappings, or metadata as needed.
Must return the decorated model class.
Source code in src/protean/port/provider.py
314 315 316 317 318 319 320 321 322 323 324 325 | |
construct_database_model_class
abstractmethod
construct_database_model_class(
entity_cls: type[Any],
) -> type[Any]
Dynamically build a DatabaseModel class for an entity.
Called when no user-defined @domain.model exists for the entity.
The framework calls this during domain initialization for every
aggregate/entity that doesn't have an explicit model mapping.
Must return a class that extends BaseDatabaseModel with
from_entity() and to_entity() implemented.
Source code in src/protean/port/provider.py
327 328 329 330 331 332 333 334 335 336 337 | |
raw
raw(query: Any, data: Any = None) -> Any
Run raw query directly on the database.
Query should be executed immediately on the database as a separate unit of work (in a different transaction context). The results should be returned as returned by the database without any intervention. It is left to the consumer to interpret and organize the results correctly.
Raises NotSupportedError if the provider does not support raw queries.
Source code in src/protean/port/provider.py
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | |
pool_stats
pool_stats() -> dict[str, int]
Return connection pool statistics.
Providers that support connection pooling should override this to
return a dict with keys size, checked_out, overflow,
and checked_in. The default implementation returns an empty
dict, signalling that no pool metrics are available.
Source code in src/protean/port/provider.py
365 366 367 368 369 370 371 372 373 | |
owns
owns(element_cls: type[Any]) -> bool
Return whether this provider materializes a table/index for element_cls.
A single, positive gate for the "does this registered element get a
table or index from this provider?" decision, used by the provider
artifact/reset loops that iterate registered elements. It returns
False for:
- Event-sourced aggregates — they persist to the event store, not a database table/index.
- Entities of event-sourced aggregates (at any nesting depth) — they follow their root aggregate into the event store.
- Cache-backed elements (e.g.
@domain.projection(cache=...)) — they persist to a cache adapter and carrymeta_.provider is None. - Elements owned by a different provider — in multi-provider domains, each provider materializes only the elements configured to it.
An element referencing a provider name that is not configured raises
ConfigurationError — a misconfiguration must fail fast at setup, not
be silently skipped.
Assumes a fully-initialized domain (meta_.aggregate_cluster is
populated during domain.init()). Ownership is resolved by comparing
provider names, so a None provider is never used to index the
provider registry and cache-backed elements never raise here.
Source code in src/protean/port/provider.py
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 | |