ldap-mapper is a typed, dataclass-based object mapper for OpenLDAP-style
directories built on top of ldap3. It keeps LDAP concepts explicit: DNs,
object classes, mapped attributes, and LDAP result handling remain visible in
the API. It is not a relational ORM.
Version 0.1 is OpenLDAP-oriented. Entry identity is read from entryUUID, and
the optional password-policy model targets OpenLDAP ppolicy attributes.
python -m pip install ldap-mapperRequires Python 3.12 or newer.
For development from a checkout:
python -m pip install -e '.[dev]'from dataclasses import dataclass
from typing import ClassVar
from ldap_mapper import LdapModel, StringField, mapped_field
@dataclass(slots=True)
class Person(LdapModel):
uid: str = mapped_field(StringField("uid"))
common_name: str = mapped_field(StringField("cn"))
surname: str = mapped_field(StringField("sn"))
mail: str | None = mapped_field(StringField("mail"), default=None)
container_dn: ClassVar[str] = "ou=people"
rdn_field: ClassVar[str] = "uid"
search_object_class: ClassVar[str] = "person"
object_classes: ClassVar[tuple[str, ...]] = (
"top",
"person",
"organizationalPerson",
"inetOrgPerson",
)LdapAdapter.add() returns a model bound to the server-assigned entryUUID.
replace() finds that UUID before writing. If the RDN has changed, it raises
LdapRenameRequired unless allow_rename=True; this preserves identity across
DN changes. get() and get_by_uuid() return None for zero entries, return
one model for one entry, and raise LdapMultipleEntriesFound otherwise.
mapped_field() supports three write and output controls:
addable=Falseomits an attribute from LDAP adds.replaceable=Falseprevents ordinary replacement writes.serializable=Falseomits the field fromto_dict().
Required mapped attributes must be present when decoding. A scalar field that LDAP returns with an invalid value raises a model error instead of silently coercing it.
Extensions attach optional object classes and their attributes to a model.
The generic mechanism lives in LdapExtension and ldap_extension.
Built-in OpenLDAP-friendly extensions are available from
ldap_mapper.extensions:
from ldap_mapper import LdapModel, ldap_extension
from ldap_mapper.extensions import LdapSshKey, LdapUnix
class Account(LdapModel):
unix = ldap_extension(LdapUnix)
ssh = ldap_extension(LdapSshKey)Assign an extension instance to attach it; assign None to remove it. On
replace, the mapper adds and removes managed object classes as needed.
LdapAdapter owns a thread-local ldap3.Connection; each thread gets its own
connection after first use. Do not share model mutation across threads without
your own synchronization.
LdapConfig.server_info defaults to "none", so the mapper does not read the
Root DSE or schema automatically. Set it to "dsa", "schema", or "all"
only when the client needs server.info or server.schema. Loading schema or
all server information adds LDAP requests and is especially costly for
short-lived user-bind connections.
authenticate(dn, password) validates credentials by binding as that user. It
returns None on success, raises LdapInvalidCredentialsError for invalid
credentials, and raises LdapOperationError when LDAP itself cannot complete
the bind.
Password operations intentionally have different inputs:
reset_password()andchange_password()accept plaintext and use RFC 3062 password modification after validatingPasswordPolicy.restore_password()accepts onlyEncodedPassword: an already encoded LDAP verifier. It writesuserPassworddirectly and does not hash plaintext.
Use LdapPasswordPolicy when your directory exposes OpenLDAP ppolicy
attributes. PasswordPolicy can also be created directly for local validation.
Adapter failures inherit from LdapAdapterError; mapping failures inherit from
LdapModelError. Public exceptions include LdapEntryNotFound,
LdapMultipleEntriesFound, LdapRenameRequired, LdapOperationError, and
password-specific errors. Import public names from ldap_mapper; metadata keys
used by mapped_field() are intentionally private.
python -m ruff format .
python -m ruff check .
python -m pyright
python -m pytestMIT. See LICENSE.