Metadata-Version: 2.4
Name: decidalo_client
Version: 0.1.1
Summary: An async Python client for the decidalo V3 Import API
Project-URL: Changelog, https://github.com/Hochfrequenz/decidalo_client.py/releases
Project-URL: Homepage, https://github.com/Hochfrequenz/decidalo_client.py
Author-email: Hochfrequenz Unternehmensberatung GmbH <info@hochfrequenz.de>
License: MIT
License-File: LICENSE
Keywords: aiohttp,api,async,client,decidalo,pydantic
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Requires-Dist: aiohttp>=3.10
Requires-Dist: pydantic>=2.5
Provides-Extra: codegen
Requires-Dist: datamodel-code-generator==0.56.1; extra == 'codegen'
Provides-Extra: coverage
Requires-Dist: coverage==7.13.5; extra == 'coverage'
Provides-Extra: dev
Requires-Dist: pip-tools; extra == 'dev'
Provides-Extra: formatting
Requires-Dist: black==26.3.1; extra == 'formatting'
Requires-Dist: isort==8.0.1; extra == 'formatting'
Provides-Extra: linting
Requires-Dist: pylint==4.0.5; extra == 'linting'
Provides-Extra: packaging
Requires-Dist: build==1.4.4; extra == 'packaging'
Requires-Dist: twine==6.2.0; extra == 'packaging'
Provides-Extra: spell-check
Requires-Dist: codespell==2.4.2; extra == 'spell-check'
Provides-Extra: tests
Requires-Dist: aioresponses==0.7.8; extra == 'tests'
Requires-Dist: pytest-asyncio==1.3.0; extra == 'tests'
Requires-Dist: pytest==9.0.3; extra == 'tests'
Provides-Extra: type-check
Requires-Dist: mypy==1.20.2; extra == 'type-check'
Requires-Dist: mypy[pydantic]; extra == 'type-check'
Description-Content-Type: text/markdown

# decidalo_client.py

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Python Versions (officially) supported](https://img.shields.io/pypi/pyversions/decidalo-client.svg)
![Pypi status badge](https://img.shields.io/pypi/v/decidalo-client)

![Unittests status badge](https://github.com/Hochfrequenz/decidalo_client.py/workflows/Unittests/badge.svg)
![Coverage status badge](https://github.com/Hochfrequenz/decidalo_client.py/workflows/Coverage/badge.svg)
![Linting status badge](https://github.com/Hochfrequenz/decidalo_client.py/workflows/Linting/badge.svg)
![Formatting status badge](https://github.com/Hochfrequenz/decidalo_client.py/workflows/Formatting/badge.svg)

This repository contains two async Python clients for [decidalo](https://decidalo.de/):

| Client | API | Purpose |
|--------|-----|---------|
| `DecidaloClient` (Import Client) | [V3 Import API](https://import.decidalo.dev/index.html) | Bulk-importing data (users, teams, projects, bookings, ...) into decidalo |
| `DecidaloAppClient` (App Client) | App API (`api.decidalo.app`) | Reading data from decidalo: searching people, viewing profiles, skills, certificates, projects |

Use the **Import Client** when you need to push data _into_ decidalo (e.g. syncing users from an HR system).
Use the **App Client** when you need to read data _from_ decidalo (e.g. finding people with specific skills).

> [!IMPORTANT]
> This is a community project and is NOT an official decidalo client.
> It is not affiliated with or endorsed by Data Assessment Solutions GmbH.

## Installation

```bash
pip install decidalo-client
```

## Import Client (`DecidaloClient`)

The Import Client wraps the decidalo V3 Import API ([Swagger UI](https://import.decidalo.dev/index.html)).
It is used for bulk-importing data into decidalo using an API key.

```python
import asyncio
from decidalo_client import DecidaloClient, DecidaloAPIError, DecidaloAuthenticationError

async def main() -> None:
    async with DecidaloClient(api_key="your-api-key") as client:
        # Get all users
        users = await client.get_users()
        for user in users:
            print(f"{user.displayName} ({user.email})")

        # Get all projects
        projects = await client.get_all_projects()
        for project in projects:
            print(f"{project.properties.name.value}")

if __name__ == "__main__":
    asyncio.run(main())
```

### Error Handling

```python
import asyncio
from decidalo_client import DecidaloClient, DecidaloAPIError, DecidaloAuthenticationError

async def main() -> None:
    async with DecidaloClient(api_key="your-api-key") as client:
        try:
            users = await client.get_users()
        except DecidaloAuthenticationError as e:
            print(f"Authentication failed: {e.message}")
        except DecidaloAPIError as e:
            print(f"API error {e.status_code}: {e.message}")

if __name__ == "__main__":
    asyncio.run(main())
```

### Import Client Features

- Async HTTP client built on `aiohttp`
- Type-safe request/response models using `pydantic`
- All major API endpoints:
  - **Users** - Get users, import users (sync/async), check import status
  - **Teams** - Get teams, import teams (sync/async), check import status
  - **Companies** - Get companies, import companies
  - **Projects** - Get projects, get all projects, import projects, check existence
  - **Bookings** - Get bookings, get bookings by project, import bookings
  - **Absences** - Get absences, import absences
  - **Resource Requests** - Get resource requests, import resource requests
  - **Roles** - Import roles
  - **Working Time Patterns** - Get working time patterns, import working time patterns

## App Client (`DecidaloAppClient`)

The App Client wraps the decidalo App API (`api.decidalo.app`).
It is used for reading data from decidalo — searching for people, viewing profiles, exploring skills, certificates, and projects.

> [!NOTE]
> The App API does not have a public Swagger UI.
> The client was reverse-engineered from the decidalo web application.

### Authentication

The App Client authenticates via OAuth2 (Microsoft SSO) through `login.decidalo.app`.
There are two authentication flows:

1. **Device Code Flow** (interactive, for first-time setup) — prints a URL and code to the console for you to open in a browser.
2. **Refresh Token Flow** (headless, for automation) — reuses a previously obtained refresh token.

```python
import asyncio
from decidalo_app_client import DecidaloAppClient
from decidalo_app_client.auth import DecidaloAuth

async def first_time_login() -> None:
    """Interactive login — run this once to obtain a refresh token."""
    token = await DecidaloAuth.device_code_login()
    # The device code flow prints a URL and code to the console.
    # Open the URL in your browser and enter the code to authenticate.
    print(f"Save this refresh token for future use: {token.refresh_token}")

asyncio.run(first_time_login())
```

Store the refresh token securely (e.g. in an environment variable or a secrets manager).
For subsequent runs, use the refresh token:

```python
token = await DecidaloAuth.refresh("your-saved-refresh-token")
```

### Minimal Working Example

```python
import asyncio
from decidalo_app_client import DecidaloAppClient
from decidalo_app_client.auth import DecidaloAuth

async def main() -> None:
    # Use a refresh token obtained from a previous device_code_login()
    token = await DecidaloAuth.refresh("your-saved-refresh-token")

    async with DecidaloAppClient(token=token) as client:
        # Search for people with specific skills
        results = await client.search.find_people(keywords=["SAP", "Python"])
        for user in results.usersWithMatchedQualities:
            print(f"User {user.userId} (Score: {user.score})")

        # Get a user's profile header
        header = await client.profile.get_header(user_id=42)
        print(f"Profile quality: {header.profileQuality}, last edited by: {header.lastEditor}")

        # Browse available skill categories
        categories = await client.skills.get_categories()
        for cat in categories:
            print(f"Category: {cat.categoryName}")

asyncio.run(main())
```

You can also pass a static Bearer token string directly if you manage tokens yourself:

```python
async with DecidaloAppClient(token="your-bearer-token") as client:
    ...
```

### App Client Features

- Async HTTP client built on `aiohttp` with automatic token refresh
- OAuth2 Device Code Flow and Refresh Token Flow (direct OIDC HTTP, no extra dependency)
- Type-safe Pydantic models for most responses
- Domain-based API structure:
  - **Search** — Find people by skills/keywords, autocomplete user names, get filter fields
  - **Profile** — Read profile headers, skills, certificates, languages, industries, roles, competencies, projects
  - **Projects** — Get project headers, overviews, details, team members, references
  - **Skills** — Autocomplete skills, get levels, categories, skill grids, assessments
  - **Certificates** — Autocomplete certificates, get holders, certificate grids
  - **Roles** — Get roles, check user skills/certificates against role requirements
  - **Teams** — Get team details, find teams by manager, get members under current user

## Development

Clone the repository and install the development environment:

```bash
git clone https://github.com/Hochfrequenz/decidalo_client.py.git
cd decidalo_client.py
tox -e dev
```

To regenerate the Pydantic models from the OpenAPI spec:

```bash
tox -e codegen
```

For detailed information on the development setup (tox configuration, IDE setup, etc.), see the [Hochfrequenz Python Template Repository](https://github.com/Hochfrequenz/python_template_repository).

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
