Metadata-Version: 2.4
Name: digiwin-iam-sdk
Version: 0.1.0
Summary: Python SDK for Digiwin IAM (Identity and Access Management) System
Author: Digiwin Middleware Team
License-Expression: MIT
License-File: LICENSE
Keywords: authentication,dapware,digiwin,iam,identity
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: cryptography>=42.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: twine>=6.0; extra == 'dev'
Description-Content-Type: text/markdown

# digiwin-iam-sdk

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)

Python SDK for the Digiwin IAM (Identity and Access Management) system.

PyPI distribution name: `digiwin-iam-sdk`

Import namespace: `dapware_iam`

## Features

- Identity, user, tenant, and org API modules
- Transparent RSA + AES login handshake
- Synchronous and asynchronous clients
- Pydantic-based request and response models
- Typed exceptions for auth, validation, server, and crypto failures

## Installation

Install from PyPI:

```bash
pip install digiwin-iam-sdk
```

Install from source for development:

```bash
pip install -e ".[dev]"
```

## Quick Start

### Synchronous client

```python
from dapware_iam import IAMClient

with IAMClient(
    base_url="https://iam-test.digiwincloud.com.cn",
    app_token="your-app-token",
) as client:
    user = client.identity.login("your_user_id", "your_password")
    print(user.token)

    token_info = client.identity.analyze_token()
    print(token_info.user_id)

    current_user = client.users.get_user(user_id=token_info.user_id)
    tenants = client.tenants.get_tenants()

    print(current_user.user_id)
    print([tenant.id for tenant in tenants])

    client.identity.logout()
```

### Asynchronous client

```python
import asyncio

from dapware_iam import AsyncIAMClient


async def main() -> None:
    async with AsyncIAMClient(
        base_url="https://iam-test.digiwincloud.com.cn",
        app_token="your-app-token",
    ) as client:
        user = await client.identity.login("your_user_id", "your_password")
        print(user.token)

        token_info = await client.identity.analyze_token()
        current_user = await client.users.get_user(user_id=token_info.user_id)
        tenants = await client.tenants.get_tenants()

        print(current_user.user_id)
        print([tenant.id for tenant in tenants])

        await client.identity.logout()


asyncio.run(main())
```

## Tenant-Scoped APIs

Some endpoints require a tenant-scoped token. In the environments validated so
far, the default login token can be used for:

- `identity.analyze_token()`
- `users.get_user(...)`
- `tenants.get_tenants(...)`

The following methods should be treated as tenant-scoped and called only after
`identity.refresh_token_with_tenant(...)` succeeds:

- `tenants.get_current()`
- `tenants.get_current_simple()`
- `tenants.get_applications(...)`
- `orgs.get_cascade()`
- `orgs.get_org_list(...)`
- `orgs.get_users_in_org(...)`
- `orgs.get_users_cascade(...)`
- `orgs.create_or_update(...)`
- `orgs.delete(...)`

Example:

```python
with IAMClient(base_url="...", app_token="...") as client:
    client.identity.login("user", "password")
    client.identity.refresh_token_with_tenant(tenant_id="your-tenant")

    tenant = client.tenants.get_current()
    org_tree = client.orgs.get_cascade()
```

If the target tenant has not purchased the current application, the backend may
reject the tenant switch with an authorization error.

## API Overview

### `IAMClient` and `AsyncIAMClient`

| Parameter | Type | Default | Description |
|---|---|---|---|
| `base_url` | `str` | - | IAM service base URL |
| `app_token` | `str` | - | `digi-middleware-auth-app` JWT |
| `timeout` | `float` | `30.0` | Request timeout in seconds |
| `verify_ssl` | `bool` | `True` | Whether to verify SSL certificates |
| `extra_headers` | `dict[str, str] \| None` | `None` | Extra headers added to every request |

### `client.identity`

| Method | Endpoint | Description |
|---|---|---|
| `login(user_id, password, ...)` | `POST /login` | Cloud user login |
| `login_ad(user_id, password, ...)` | `POST /login` | Digiwin AD login |
| `login_verification_code(phone, code)` | `POST /login` | SMS code login |
| `internal_login(tenant_id, user_id, password, ...)` | `POST /internal/login` | Enterprise internal login |
| `logout(clear_all=False)` | `POST /logout` | Logout |
| `get_public_key()` | `GET /publickey` | Get the server RSA public key |
| `get_aes_key(encrypted_key)` | `POST /aeskey` | Exchange AES key |
| `analyze_token()` | `POST /token/analyze` | Parse the current token |
| `analyze_token_internal()` | `GET /token/analyze/internal` | Parse token with the internal variant |
| `get_login_info()` | `POST /login/info` | Get login info for the current token |
| `refresh_token_with_tenant(...)` | `POST /token/refresh/tenant` | Switch tenant and refresh token |
| `refresh_user_token()` | `POST /token/refresh/user` | Refresh user token |
| `create_access_token(...)` | `POST /token/grant/access` | Create an SSO access token |

### `client.users`

| Method | Endpoint | Description |
|---|---|---|
| `get_user(user_id=None, user_sid=0)` | `POST /api/iam/v2/user` | Query one user |
| `get_user_list(user_ids)` | `POST /user/list` | Batch query users in the current tenant |
| `query_users(query_type, ...)` | `POST /user/query` | Query users by org or status |
| `get_user_simple()` | `GET /user/simple` | Query current-tenant simple users |
| `check_email_exists(email)` | `POST /user/email/exist` | Check whether email is registered |
| `check_phone_exists(phone)` | `POST /user/mobilephone/exist` | Check whether phone is registered |
| `check_user_exists(user_id)` | `POST /user/exists` | Check whether a user exists |
| `get_user_roles(...)` | `POST /user/role` | Query user roles |

### `client.tenants`

| Method | Endpoint | Description |
|---|---|---|
| `get_tenants(...)` | `POST /api/iam/v2/tenant` | List the current user's available tenants |
| `get_current()` | `POST /tenant/current` | Get current tenant detail |
| `get_simple(tenant_id)` | `POST /tenant/simple` | Get one tenant's simple info |
| `get_current_simple()` | `GET /tenant/current/simple` | Get current tenant simple info |
| `query_tenants(keyword)` | `POST /tenant/query` | Search open tenants |
| `check_user_in_tenant(user_id)` | `GET /tenant/check/userintenant` | Check whether a user exists in the current tenant |
| `get_applications(...)` | `GET /tenant/application` | Get current tenant applications |
| `invite_user(...)` | `POST /tenant/user/invite/new` | Invite a user into a tenant |
| `remove_user(...)` | `POST /tenant/remove/user` | Remove a user from a tenant |

### `client.orgs`

| Method | Endpoint | Description |
|---|---|---|
| `get_cascade()` | `GET /org/cascade` | Get org tree |
| `get_org_list(org_sids)` | `POST /org/list` | Batch query orgs by sid |
| `get_users_in_org(...)` | `POST /org/userinorg` | Query users in one org |
| `get_users_cascade(...)` | `POST /org/userinorg/cascade` | Query users in an org subtree |
| `create_or_update(org_info)` | `POST /org/update` | Create or update an org |
| `delete(org_sid)` | `POST /org/del` | Delete an org |

## Error Handling

```python
from dapware_iam import AuthenticationError, IAMClient, ServerError

with IAMClient(base_url="...", app_token="...") as client:
    try:
        client.identity.login("user", "wrong_password")
    except AuthenticationError as exc:
        print(exc)
        print(exc.status_code)
        print(exc.body)
    except ServerError as exc:
        print(exc)
```

Exception hierarchy:

```text
IAMError
|- AuthenticationError
|- AuthorizationError
|- NotFoundError
|- ValidationError
|- ServerError
`- CryptoError
```

## Configuration

Disable SSL verification only for trusted internal environments that use
self-signed certificates:

```python
client = IAMClient(
    base_url="https://internal-iam.company.com",
    app_token="...",
    verify_ssl=False,
)
```

## Development And Release

Manual release instructions live in `RELEASING.md`. The short version is:

```bash
python -m pytest -q
python -m build
python -m twine check dist/*
```

Upload to TestPyPI first, validate the exact built artifacts, then upload the
same files to PyPI.

## License

MIT
