udm-rest-api-client 0.1.2
Client library to interact with the Univention Directory Manager (UDM) REST API.
Requires Python: >=3.11

# UDM REST API Client

[![PyPI - Version](https://img.shields.io/pypi/v/udm-rest-api-client.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.python.org/pypi/udm-rest-api-client/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/udm-rest-api-client.svg?logo=python&label=Python&logoColor=gold)](https://test.pypi.python.org/pypi/udm-rest-api-client)
[![License - AGPLv3](https://img.shields.io/github/license/univention/univention-corporate-server)](https://github.com/univention/univention-corporate-server/blob/5.2-0/LICENSE)
[![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy)

Client library and CLI to interact with the [Univention Directory Manager (UDM) REST API](https://docs.software-univention.de/developer-reference/latest/en/udm/rest-api.html).

## Features

* Asynchronous and synchronous Python functions
* Command line interface
* Automatic handling of HTTP(S) sessions
* Type annotations
* Python 3.11+

## Usage

### Synchronous Python code

```python
from univention.admin.rest.client import UDM

udm = UDM.http("https://10.20.30.40/univention/udm/", "Administrator", "s3cr3t")
module = udm.get("users/user")

# 1. create a user
obj = module.new()
obj.properties["username"] = "foo"
obj.properties["password"] = "univention"
obj.properties["lastname"] = "foo"
obj.save()

# 2. search for users
for result in module.search("uid=*"):
    obj = result.open()
    print(obj)
    print(obj.properties)
    print(obj.objects.groups)

# 3. get by dn
ldap_base = udm.get_ldap_base()
obj = module.get(f"uid=foo,cn=users,{ldap_base}")

# 4. get referenced objects e.g. groups
pg = obj.objects["primaryGroup"][0].open()
print(pg.dn, pg.properties)
print(obj.objects["groups"])

# 5. modify
obj.properties["description"] = "foo"
obj.save()

# 6. move to the ldap base
obj.move(ldap_base)

# 7. remove
obj.delete()
```

### Asynchronous Python code

```python
import asyncio
from univention.admin.rest.async_client import UDM

async def main():
    async with UDM.http("http://10.20.30.40./univention/udm/", "Administrator", "s3cr3t") as udm:
        module = await udm.get("users/user")

        # 1. create a user
        obj = await module.new()
        obj.properties["username"] = "foo"
        obj.properties["password"] = "univention"
        obj.properties["lastname"] = "foo"
        await obj.save()

        # 2. search for users
        async for result in module.search("uid=*"):
            obj = await result.open()
            print(obj)
            print(obj.properties)
            print(obj.objects.groups)

        # 3. get by dn
        ldap_base = await udm.get_ldap_base()
        obj = await module.get(f"uid=foo,cn=users,{ldap_base}")

        # 4. get referenced objects e.g. groups
        pg = await obj.objects["primaryGroup"][0].open()
        print(pg.dn, pg.properties)
        print(obj.objects["groups"])

        # 5. modify
        obj.properties["description"] = "foo"
        await obj.save()

        # 6. move to the ldap base
        await obj.move(ldap_base)

        # 7. remove
        await obj.delete()

asyncio.run(main())
```

### Custom Request ID Generation

By default, the UDM client generates unique request IDs using `uuid.uuid4().hex` for each HTTP request. You can provide a custom request ID generator function to control how request IDs are created:

```python
from univention.admin.rest.client import UDM
import time

# Custom request ID generator using timestamp and counter
counter = 0
def custom_request_id():
    global counter
    counter += 1
    return f"req-{int(time.time())}-{counter}"

# Use with synchronous client
udm = UDM.http(
    "https://10.20.30.40/univention/udm/",
    "Administrator",
    "s3cr3t",
    request_id_generator=custom_request_id
)

# Use with bearer token authentication
udm = UDM.bearer(
    "https://10.20.30.40/univention/udm/",
    "your-bearer-token",
    request_id_generator=custom_request_id
)
```

For asynchronous clients:

```python
from univention.admin.rest.async_client import UDM

async def main():
    async with UDM.http(
        "http://10.20.30.40./univention/udm/",
        "Administrator",
        "s3cr3t",
        request_id_generator=lambda: f"async-{uuid.uuid4().hex[:8]}"
    ) as udm:
        # Your async code here
        pass
```

The `request_id_generator` function should return a string that will be used as the `X-Request-Id` header value. If the function returns `None`, no request ID will be added to that particular request.

### Command line interface

```shell
PASS_FILE="$HOME/pw-$(date +%Y%m%d%H%M%S)"
echo "s3cr3t" > "$PASS_FILE"

udm \
  --username Administrator \
  --bindpwdfile "$PASS_FILE" \
  --uri http://10.20.30.40/univention/udm/ \
  users/user list \
  --filter uid=Administrator

rm -f "$PASS_FILE"
```

Instead of using `udm`, the CLI can also be called using `python3 -m univention.admin.rest.client`.

### Error codes

Error codes and other details of the UDM REST API can be found in its [documentation](https://docs.software-univention.de/developer-reference/latest/en/udm/rest-api.html#api-error-codes).

## Installation

The dependencies for the CLI, synchronous and asynchronous clients differ.
They are available separately as "extra" dependencies, to reduce your projects total dependencies.
Without installing the "extra" requirements, only the common dependencies of the three interfaces will be installed, and _none_ will be usable.

* `async`: installs `aiohttp`
* `sync`: installs `requests`
* `cli`: installs `python-ldap` and `requests`

To install the library including the dependencies for the synchronous client via pip from PyPI run:

```shell
pip install udm-rest-api-client[sync]
```

To install the library including the dependencies for the asynchronous client via pip from PyPI run:

```shell
pip install udm-rest-api-client[async]
```

To install the CLI (incl. the required library and its dependencies):

```shell
pip install udm-rest-api-client[cli]
```

Multiple extras can be specified at the same time:

```shell
pip install udm-rest-api-client[async,cli,dev,sync]
```

### CLI and pipx

If you wish to use the `udm` command line interface from anywhere in your system,
without having to manually handle virtual environments,
install it using [pipx](https://pipx.pypa.io):

```shell
pipx install udm-rest-api-client[cli]
```

```shell
which udm
$HOME/.local/bin/udm
```

## Testing

### Using Docker Compose

```shell
docker compose -f tests/docker-compose.yml run --rm test
docker compose -f tests/docker-compose.yml down -v
```

### Locally

```shell
pip install -e '.[async,sync,dev,cli]'

docker compose -f tests/docker-compose.yml up -d ldap-server udm-rest-api

export UDM_URL=http://127.0.0.1:9979/udm/
pytest

docker compose -f tests/docker-compose.yml down -v
```

## Building and publishing

There is a Makefile that offers all required commands.
A help text is available, when started without arguments:

```shell
$ make

clean                remove all build and Python artifacts
clean-build          remove build artifacts
clean-pyc            remove Python file artifacts
build                builds source and wheel package
publish              package and upload a release to pypi
publish-test         package and upload a release to the pypi test site
install-hatch        install 'hatch' using 'pipx'
```

The build system uses [hatch](https://hatch.pypa.io/).
To install it, run `make install-hatch`.
Then execute `make publish-test` to build and publish the client to https://test.pypi.org/project/udm-rest-api-client/ or `make publish` to publish to https://pypi.org/project/udm-rest-api-client/ .

## License

The _UDM REST API Client_ is distributed under the terms of the [GNU Affero General Public License v3.0 only (AGPLv3)](https://spdx.org/licenses/AGPL-3.0-only.html) license.

## Changelog

udm-rest-api-client (0.1.2) unstable; urgency=low

  * Add missing dependency for Debian packages


... see [here](https://github.com/univention/univention-corporate-server/blob/8f9ad22f119c1564ea9ffad4885a3bf7610e026d/management/univention-directory-manager-rest/debian/changelog) for earlier changes.