morpheus-data-layer
Import prefix morpheus_data_layer. Layer MorpheusDataLayer. Used by all five Lambda
functions; not used by the AgentCore runtime, which has no database access.
Source: libs/morpheus-data-layer/morpheus_data_layer/.
The schema is owned by the Payability web application’s Prisma schema; this library is a read/write ORM over a subset of it. See Prisma schema reference.
session.py
A module-level engine singleton, so one connection pool serves the whole process — whether that is a warm Lambda container, the AgentCore runtime, or a script.
def init(database_url: str, *, pool_size: int = 5, max_overflow: int = 2) -> None
@contextmanager
def get_session() -> Generator[Session, None, None]
def check_connection() -> bool
init() is idempotent: a second call is a no-op and the existing engine and pool are kept
intact.
The layer installs psycopg2-binary only. init() therefore always names the driver:
postgresql://, postgres://, and postgresql+psycopg:// are rewritten to
postgresql+psycopg2:// (the last with a warning). Without this, SQLAlchemy 2.1+ resolves a
bare postgresql:// to psycopg 3 and the cold start fails with
No module named 'psycopg'. SQLAlchemy is also pinned to >=2.0,<2.1 in
requirements.txt, the file SAM installs the layer from, so the layer matches the version
in uv.lock that the tests run against.
Pooling
pool_size |
Behaviour |
|---|---|
|
|
|
|
|
Always Each Lambda container handles one invocation at a time and gains nothing from a persistent
pool, while Equally: use the Supabase Transaction mode pooler URL (port |
Every new connection runs SET search_path TO public through a connect event listener.
Transaction boundary
with get_session() as session:
seller = _seller_repo.get_by_seller_id(session, seller_id)
_seller_repo.update(session, seller, {"lifeCycleStage": SellerLifeCycleStage.ENRICHED})
# committed here
Commits on clean exit; rolls back and re-raises on any exception; always closes. The session
factory uses expire_on_commit=False, so scalar attributes stay readable on a detached
object after the block ends.
|
Two consequences of
|
check_connection() runs SELECT 1 and returns a boolean — for health checks.
models/
One SQLAlchemy 2.0 declarative model per file, all on a shared Base, exported from
models/init.py.
| Model | Table |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Conventions:
-
Attribute names are camelCase and match the database column exactly. No
name=override inmapped_column— the Python and SQL names are identical, which is unusual for SQLAlchemy but keepsconflict_columnslists and raw SQL honest. -
ForeignKey("subscriptions.id")is not declared. That table is not managed here; the constraint is enforced at the database level only. -
One model per file, always exported from
models/init.py.
Details: ORM models.
repositories/
Stateless CRUD objects, instantiated once at module level by consumers:
_seller_repo = SellerRepository() # module level — stateless, safe to share
They never open sessions and never commit. Every method takes a Session as its first
argument.
BaseRepository[ModelT]
| Method | Behaviour |
|---|---|
|
|
|
Paged select |
|
Equality filters, all matches |
|
Equality filters, first match |
|
Add, flush, refresh, return the instance |
|
Core |
|
PostgreSQL |
|
|
|
Fetch then update; |
conflict_columns and update_columns take camelCase database column names, matching the
ORM attributes.
Concrete repositories
Each adds query methods for the access patterns the handlers actually need. Notable ones:
| Repository | Domain-specific methods |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
EmailRepository and EmailVariationRepository add nothing beyond the base.
Full signatures: Repositories.
Round-robin selection
repositories/sales_rep_round_robin.py provides select_next_sales_rep_round_robin(),
reserve_sales_rep_round_robin_batch() and dialer_rr_config_key(). These are functions,
not repository methods, because they span sales_reps and configs and take a PostgreSQL
advisory transaction lock. See Ownership and round-robin.
reserve_sales_rep_round_robin_batch(session, subscription_id, , require_nooks, count)
reserves count reps in one call, advancing the cursor once. Prefer it when assigning many
records at once: select_next_sales_rep_round_robin() (which now delegates to it with
count=1) costs a rep query, an advisory lock and a cursor write *per call, so calling it in
a loop over thousands of rows will exhaust a Lambda timeout.
enums.py
StrEnum mirrors of the Prisma/PostgreSQL enums, so a comparison against a string literal
works but the canonical member is available: SellerPlatform, SellerDatasource,
SellerLifeCycleStage, ContactHubspotSyncStatus, ContactSource, ContactHeatStatus,
ContactLifecycleStage, ContactLeadStatus, EmailTargetStatus, EmailTargetCategory,
EmailDirection, EmailInboxCategory, EmailVariationStatus, LineTypes, RiskLevel,
CallStatus, TargetState, FollowUpTaskType, FollowUpTaskStatus, ActivityType.
Note LineTypes uses camelCase values (fixedVoip, nonFixedVoip, tollFree) because
they mirror Twilio’s vocabulary exactly. Full listing:
Enumerations.
Adding a model or repository
-
Create
models/<name>.pywith camelCase attributes matching the columns exactly. Nosubscriptionsforeign key. -
Export it from
models/init.py. -
Create
repositories/<name>.pyextendingBaseRepository[YourModel]withmodel_class = YourModel. -
Add only the query methods the callers need. No sessions, no commits.
-
Export it from
repositories/init.py. -
Add a test under
tests/.
Tests
cd libs/morpheus-data-layer && uv run pytest tests -v
tests/integration/ (test_connection.py, test_seller_repository.py,
test_configs_repository.py) needs a reachable database and reads its connection string
from libs/morpheus-data-layer/.env. A Makefile in the library directory wraps the common
commands.