Metadata-Version: 2.4
Name: sap_commerce_cloud_management_apis
Version: 1.0.0
Summary: A client library for accessing SAP Commerce Cloud - Management API
Requires-Python: >=3.11,<4.0
Classifier: Programming Language :: Python :: 3
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-Dist: attrs (>=22.2.0)
Requires-Dist: httpx (>=0.23.1,<0.29.0)
Description-Content-Type: text/markdown

# sap_commerce_cloud_management_apis

A Python client library for the **SAP Commerce Cloud — Cloud Portal APIs** (a.k.a. the
Management APIs). It lets you automate operational tasks against your Commerce Cloud
subscription — listing environments, triggering deployments, managing data backups,
endpoints, TLS certificates, scaling, scheduled activities, and user role assignments —
from Python instead of the Cloud Portal UI or CLI.

Built on [`httpx`](https://www.python-httpx.org/) and [`attrs`](https://www.attrs.org/);
every request and response is typed.

- **Base URL:** `https://portalapi.commerce.ondemand.com/v2`
- **Format:** REST / JSON
- **Auth:** OAuth2 client-credentials bearer token, sent in the **`x-approuter-authorization`** header
- **Reference:** [Cloud Portal API Documentation](https://help.sap.com/docs/SAP_COMMERCE_CLOUD_PUBLIC_CLOUD/452dcbb0e00f47e88a69cdaeb87a925d/66abfe678b55457fab235ce8039dda71.html)

> ⚠️ **Two things that trip people up** — read these before your first call:
> 1. The token goes in `x-approuter-authorization`, **not** the standard `Authorization` header.
>    You must pass `auth_header_name="x-approuter-authorization"` (see below).
> 2. The `base_url` must include the `/v2` suffix — endpoint paths are relative to it.

## Installation

This project uses [Poetry](https://python-poetry.org/):

```bash
poetry install
```

To use it from another project, either `poetry add <path-to-this-client>`, or build a wheel
(`poetry build -f wheel`) and `pip install` it.

## Authentication

### 1. Obtain a bearer token

Create a **technical user** in the Cloud Portal (see
[Technical Users](https://help.sap.com/docs/SAP_COMMERCE_CLOUD_PUBLIC_CLOUD/452dcbb0e00f47e88a69cdaeb87a925d/57bef96f18034193af93d2cc36f6d526.html)),
then exchange its credentials for a token using the OAuth2 client-credentials grant:

```python
import httpx

token_response = httpx.post(
    TOKEN_URL,  # your token endpoint
    headers={"Accept": "application/json"},
    data={
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "grant_type": "client_credentials",
        "resource": RESOURCE,  # the required SAP "resource" parameter
    },
)
access_token = token_response.json()["access_token"]  # valid ~3600s
```

### 2. Build an authenticated client

```python
from sap_commerce_cloud_management_apis import AuthenticatedClient

client = AuthenticatedClient(
    base_url="https://portalapi.commerce.ondemand.com/v2",
    token=access_token,
    auth_header_name="x-approuter-authorization",  # REQUIRED for Cloud Portal
)
```

Without `auth_header_name`, the token defaults to the standard `Authorization` header, which
the Cloud Portal approuter does **not** accept.

### Loading credentials from `.env`

Never hard-code secrets. Copy [`.env.example`](.env.example) to `.env` (gitignored) and load it:

```bash
CCV2_SUBSCRIPTION_CODE=your-subscription-code
CCV2_TOKEN_URL=https://<tenant>.accounts.ondemand.com/oauth2/token
CCV2_CLIENT_ID=your-client-id
CCV2_CLIENT_SECRET=your-client-secret
SAP_CCV2_RESOURCE=urn:sap:identity:application:provider:name:...
```

```python
import os
from dotenv import load_dotenv

load_dotenv()
subscription_code = os.environ["CCV2_SUBSCRIPTION_CODE"]
# ...etc
```

## Calling the API

Every operation lives under `sap_commerce_cloud_management_apis.api.<tag>.<operation>` and
exposes four functions:

| Function | Blocking? | Returns |
| --- | --- | --- |
| `sync` | yes | parsed model, or `None` |
| `sync_detailed` | yes | `Response[T]` (status code, headers, raw content, `parsed`) |
| `asyncio` | no | parsed model, or `None` |
| `asyncio_detailed` | no | `Response[T]` |

Path/query params and request bodies are keyword/positional arguments. Most operations take
`subscription_code` and often `environment_code`.

### Example: list environments

```python
from sap_commerce_cloud_management_apis.api.environment import get_environments
from sap_commerce_cloud_management_apis.models import EnvironmentDetailsDTO, ErrorDTO

with client as c:
    result = get_environments.sync(subscription_code, client=c)

if isinstance(result, EnvironmentDetailsDTO):
    for env in result.value or []:
        print(env.code, env.type_, env.status)
elif isinstance(result, ErrorDTO):
    print("API error:", result.title, "-", result.detail)
```

Use `sync_detailed` when you need the status code or headers:

```python
from sap_commerce_cloud_management_apis.types import Response

resp: Response = get_environments.sync_detailed(subscription_code, client=client)
print(resp.status_code)   # e.g. 200
env_details = resp.parsed
```

### Example: trigger a deployment

```python
from sap_commerce_cloud_management_apis.api.deployment import create_deployment
from sap_commerce_cloud_management_apis.models import CreateDeploymentRequestDTO
from sap_commerce_cloud_management_apis.models.create_deployment_request_dto_database_update_mode import (
    CreateDeploymentRequestDTODatabaseUpdateMode,
)
from sap_commerce_cloud_management_apis.models.create_deployment_request_dto_strategy import (
    CreateDeploymentRequestDTOStrategy,
)

body = CreateDeploymentRequestDTO(
    build_code="20240101.1",
    environment_code="d1",
    database_update_mode=CreateDeploymentRequestDTODatabaseUpdateMode.NONE,
    strategy=CreateDeploymentRequestDTOStrategy.ROLLING_UPDATE,
)

deployment = create_deployment.sync(subscription_code, client=client, body=body)
```

### Example: create a data backup

```python
from sap_commerce_cloud_management_apis.api.databackup import create_databackup
from sap_commerce_cloud_management_apis.models import CreateDatabackupRequestDTO

body = CreateDatabackupRequestDTO(description="pre-release snapshot", databackup_type="STANDARD")
created = create_databackup.sync(subscription_code, "d1", client=client, body=body)
```

### Async

Every operation has an async twin — use `asyncio` / `asyncio_detailed` inside an `async with`:

```python
async with client as c:
    result = await get_environments.asyncio(subscription_code, client=c)
```

### Pagination

Paginated list endpoints (e.g. `deployment.get_deployments`) cap out at **100 items per page**
and accept OData-style params: `top`, `skip`, `orderby`, `count`.

```python
from sap_commerce_cloud_management_apis.api.deployment import get_deployments

page = get_deployments.sync(subscription_code, client=client, environment_code="d1", top=100, skip=0)
```

## Available operations

| Tag | Module | Operations |
| --- | --- | --- |
| Environments | `api.environment` | list environments |
| Deployments | `api.deployment` | create / get / cancel deployments, deployment decisions & progress, traffic split |
| Data backups | `api.databackup` | create/get/delete backups, create/get restores, change states |
| Endpoints | `api.endpoint` | create / get / update / delete endpoints |
| Scaling | `api.environment_scaling` | get / update scaling details & options |
| Scheduled activities | `api.scheduled_activity` | create / get / update / cancel scheduled activities |
| Service properties | `api.service_properties` | get / put a property |
| TLS certificates | `api.ssl_certificate` | create / get / delete certificates |
| User role assignments | `api.user_role_assignments` | list roles, create / get / update / delete assignments |

> Note: `build`-related DTOs exist in `models/` but the `build` API module is not generated in
> this client.

## Error handling

Documented failures (`4xx`/`5xx`) parse into an `ErrorDTO`
([RFC 7807](https://tools.ietf.org/html/rfc7807)) with `title` and `detail` fields — always
branch on the return type rather than assuming success.

⚠️ **Known caveat:** the generated parsers call `response.json()` on error responses. When the
approuter returns a **non-JSON** body (for example a plain-text `Bad Request` on a malformed
token), this raises `json.JSONDecodeError`. For hard-failure paths (bad/absent token), inspect
the raw HTTP status instead — e.g. issue the request with `httpx` directly, or wrap the call in
a `try/except json.JSONDecodeError`.

## TLS

Public Cloud Portal APIs require **TLS 1.2+**. Certificate verification is on by default. To use
a custom CA bundle, pass `verify_ssl="/path/to/bundle.pem"`; disabling it (`verify_ssl=False`) is
a security risk and not recommended.

## Testing

Tests live in [`tests/`](tests/) and split into two kinds:

- **Contract tests** (offline) — drive the real client through an `httpx.MockTransport`, asserting
  method, URL, the `x-approuter-authorization` header, query/body serialization, and
  response→DTO parsing. No network, no credentials.
- **Live integration tests** (marked `integration`) — perform the real OAuth2 token exchange and
  hit your tenant. They load credentials from `.env` and **skip automatically** if any variable is
  missing.

```bash
poetry run pytest                      # everything (live tests run only if .env is present)
poetry run pytest -m "not integration" # offline contract tests only
poetry run pytest -m integration       # live tests against your tenant (needs .env)
```

## Advanced customization

You can customize the underlying `httpx` client — e.g. to log every request/response:

```python
def log_request(request):
    print(f"→ {request.method} {request.url}")

def log_response(response):
    print(f"← {response.status_code} {response.request.url}")

client = AuthenticatedClient(
    base_url="https://portalapi.commerce.ondemand.com/v2",
    token=access_token,
    auth_header_name="x-approuter-authorization",
    httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
)
```

Other useful knobs on `AuthenticatedClient`: `timeout`, `follow_redirects`, `raise_on_unexpected_status`
(raise on undocumented status codes instead of returning `None`), plus `with_headers()` /
`with_cookies()` / `with_timeout()` to derive a modified copy. See the class docstring in
[`client.py`](sap_commerce_cloud_management_apis/client.py) for the full list.

## Building / publishing

This project uses Poetry:

1. Bump `version` (and other metadata) in `pyproject.toml`.
2. Lint: `ruff check .` (line length 120; rules `F`, `I`, `UP`).
3. Build a wheel: `poetry build -f wheel`.
4. Publish: `poetry publish --build` (add `-r <repo>` for a private repository configured via
   `poetry config repositories.<repo> <url>` and `poetry config http-basic.<repo> <user> <pass>`).

