diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md new file mode 100644 index 0000000..6861531 --- /dev/null +++ b/.claude/skills/review-pr/SKILL.md @@ -0,0 +1,281 @@ +--- +name: review-pr +description: Review a pull request against all cloud-sdk-python contribution standards. Use when you want to review a PR, check if a PR is ready to merge, or get structured feedback on pending changes. +tools: Bash, Read +compatibility: gh CLI ≥ 2.0, git, GitHub access to SAP/cloud-sdk-python +--- + +# PR Review: SAP Cloud SDK for Python + +Reviews a PR against 23 criteria across 6 sections. Run from the root of the `cloud-sdk-python` repository. + +--- + +## Phase 1: Identify the PR + +Determine `REPO` and `NUMBER` from what the user provided: + +- **Full GitHub URL** (e.g. `https://github.com//cloud-sdk-python/pull/1`): + parse `REPO=/cloud-sdk-python`, `NUMBER=1`. +- **`owner/repo#number`** (e.g. `/cloud-sdk-python#1`): + parse accordingly. +- **Plain number** (e.g. `93`): + `REPO=SAP/cloud-sdk-python`, `NUMBER=93`. +- **Nothing provided**: list open PRs from the default repo and ask the user to pick one: + ```bash + gh pr list --repo SAP/cloud-sdk-python --state open --json number,title,author,headRefName + ``` + +Use `REPO` and `NUMBER` in every subsequent command. + +--- + +## Phase 2: Gather Data + +**Step 1** — Run all five commands **in parallel**: + +```bash +gh pr view --repo --json number,title,body,state,labels,headRefName,baseRefName,author,commits,reviews,reviewRequests,files,additions,deletions +``` +```bash +gh pr diff --repo +``` +```bash +gh pr checks --repo 2>/dev/null || echo "CI checks not yet available" +``` +```bash +gh pr view --repo --comments +``` +```bash +gh pr view --repo --json commits --jq '.commits[].messageHeadline' +``` + +**Step 2** — Fetch each changed file at the PR head ref for accurate line references. + +From the `files` array in Step 1, for each non-binary file run (in parallel): + +```bash +mkdir -p /tmp/pr/$(dirname ) +gh api "repos//contents/?ref=" \ + -H "Accept: application/vnd.github.raw+json" \ + > /tmp/pr/ +``` + +Use the head **commit SHA** (not the branch name) as the ref — branch names are ambiguous for forks. + +Skip files larger than 500 KB or with binary extensions (`.png`, `.jpg`, `.whl`, `.gz`, etc.). + +This gives you the full file content at the exact PR state, which you will use in Phase 3 for all `file:line` citations. + +--- + +## Phase 3: Evaluate 23 Criteria + +Assign each: **✅ Pass** / **⚠️ Warning** / **❌ Fail** / **➖ N/A** + +For every `file:line` reference: use `Read /tmp/pr/` on the files fetched in Phase 2. Line numbers in that output are exact. Do not derive line positions from diff hunk offsets — diff arithmetic is unreliable. + +If you need to verify a specific rule, read the authoritative source directly: +- `CONTRIBUTING.md` +- `docs/GUIDELINES.md` +- `docs/DEVELOPMENT.md` +- `.github/pull_request_template.md` +- `.github/workflows/` (exact CI job names) + +--- + +### Section A: Process & Compliance + +**A1: PR template complete** +Template requires: Description, Related Issue, Type of Change (one box ticked), How to Test (numbered steps), checklist of 9 items all ticked. Empty or placeholder body → ❌. + +**A2: Conventional Commits** +Every commit headline must match `type(scope): description`. Types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `perf`, `style`, `build`, `revert`. PR title is also validated. Check `commit-validation` CI job. Quote failing commit titles. + +**A3: Issue linked** +PR body must contain `Closes #N`, `Fixes #N`, or `Resolves #N`. + +**A4: AI-generated code disclosure** +If the diff looks AI-generated, the PR description must explicitly disclose it and reference the [SAP AI contribution guideline](https://github.com/SAP/.github/blob/main/CONTRIBUTING_USING_GENAI.md). Required by `CONTRIBUTING.md`. + +--- + +### Section B: Security & Sensitive Data + +**B1: No sensitive data in code** +Scan diff for: hardcoded credentials/tokens/API keys, SAP-internal URLs (non-public hostnames or internal tooling URLs that should not be in a public repo), tenant IDs as literals, customer names, environment-specific configs. Any hit → ❌. + +**B2: No sensitive data in PR body** +Check the PR body for the same categories as B1: account-specific URLs containing GUIDs or subaccount identifiers, real tenant IDs, internal email addresses, internal tooling references (e.g., Slack channels, internal issue trackers). The PR template disclaimer applies. + +--- + +### Section C: Code Quality + +**C1: CI checks passing** +| Job name | Meaning | +|---|---| +| `Code Quality Checks` | ruff lint + ruff format + ty type check | +| `Unit Tests with Coverage` | pytest, coverage ≥ 80% | +| `Build SDK` | `uv build` produces `.whl` + `.tar.gz` | +| `commit-validation` | commitlint on all commits | +| `Enforce version bump when src/ is modified` | version must increase if `src/` changed | +| `Verify generated proto code is up-to-date` | only for proto changes | +| `Analyze (python)` | CodeQL security scan | + +Any required job ❌ → overall ❌. Pending → ⚠️. + +**C2: Version bump** +If diff touches any `src/` file: `version` in `pyproject.toml` MUST be incremented (semver). Only `docs/`, `tests/`, `.github/`, `.claude/` changed → ➖ N/A. + +**C3: Type hints** +All new/modified public functions, methods, class attributes must have full annotations (params + return type). New modules need `py.typed`. Missing `Optional`, `Union`, or return type on a public method → ⚠️ or ❌. + +**C4: No hardcoded values** +No magic strings (e.g., `/etc/secrets/appfnd` inline) or magic numbers. Use module-level constants or enum values. + +**C5: Import organization** +Top-level imports preferred (PEP 8). Lazy imports inside functions are a smell without a documented circular-import reason. No `requirements-*.txt` for deps already in `pyproject.toml`. + +**C6: Naming conventions** +- Enum values: `SCREAMING_SNAKE_CASE = "snake_case_value"` (e.g., `AGENT_GATEWAY = "agent_gateway"`) +- Enum members and module-level lists: alphabetical order +- Private methods/attributes/modules: leading underscore +- File names: `snake_case.py` +- GitHub workflow files: `.yaml` extension, not `.yml` + +**C7: No unused code** +No unused imports, variables, or dead methods introduced. + +**C8: No unjustified new dependencies** +New runtime dep in `pyproject.toml`: must be minimal, justified, checked for CVEs. No duplicate `requirements-*.txt` alongside `pyproject.toml`. + +**C9: Proto code freshness** +If `.proto` files under `src/sap_cloud_sdk/core/auditlog_ng/proto/` changed: "Verify generated proto code is up-to-date" CI job must pass. + +--- + +### Section D: API & Design + +**D1: API future-proofing** +New config/behavior options should be a `*Config` dataclass, not individual params. Enums over bare string constants. `create_client()` factory present; direct constructor warns users to use factory instead. + +**D2: Public API hygiene** +`__init__.py` and `__all__` expose only genuinely public symbols. Internal helpers prefixed `_`. No unnecessary wrapper classes (wrapping `requests.Session` 1:1 without adding value). + +**D3: Breaking changes properly marked** +Breaking = removing/renaming a public function/method/param, changing return type, making optional param required. If present and NOT marked with the "Breaking Changes" checkbox + migration section → ❌. + +**D4: Pagination & tenant filtering consistency** +New list/query operations: encapsulate pagination params like existing modules (see `destination`). Tenant-scoped operations: filter by tenant property for consistency. + +**D5: Telemetry instrumentation** +New client methods: `@record_metrics(Module.X, Operation.Y)` from `core/telemetry`. New module: constant added to `core/telemetry/module.py` and operations to `core/telemetry/operation.py`. If module is called by other SDK modules: `_telemetry_source: Optional[Module] = None` param present. + +--- + +### Section E: Tests & Documentation + +**E1: Tests added/updated** +Every changed `src/` file → corresponding change in `tests/`. Unit: `tests/[module]/unit/test_*.py`. Integration (BDD): `tests/[module]/integration/*.feature` + `test_*_bdd.py`. Test names: `test___`. New env vars for integration tests documented in `.env_integration_tests`. + +**E2: Documentation quality** +New modules: `user-guide.md` with overview, quick start, config examples, API examples, troubleshooting. Changed public APIs: docstrings updated (Google/NumPy style: `Args:`, `Returns:`, `Raises:`). Sub-audience features not mixed into the general user guide. + +**E3: Module structure compliance** +New modules follow: +``` +src/sap_cloud_sdk/[module]/ +├── __init__.py (create_client(), __all__) +├── client.py (or {service}_client.py) +├── config.py (load_from_env_or_mount(), *Config dataclass) +├── exceptions.py (exception hierarchy) +├── _models.py (Pydantic models) +├── py.typed (empty PEP 561 marker) +└── user-guide.md + +tests/[module]/unit/ +tests/[module]/integration/ (optional, BDD) +``` + +--- + +## Phase 4: Report + +```markdown +## PR Review: #: + +**Author**: <author> **Branch**: `<headRef>` → `<baseRef>` +**Verdict**: ✅ Ready to Merge | ⚠️ Needs Minor Work | ❌ Blocked +**Summary**: <one sentence> + +--- + +### A: Process & Compliance +| # | Criterion | Status | Finding | +|---|-----------|--------|---------| +| A1 | PR template complete | | | +| A2 | Conventional Commits | | | +| A3 | Issue linked | | | +| A4 | AI-generated code disclosure | | | + +### B: Security & Sensitive Data +| # | Criterion | Status | Finding | +|---|-----------|--------|---------| +| B1 | No sensitive data in code | | | +| B2 | No sensitive data in PR body | | | + +### C: Code Quality +| # | Criterion | Status | Finding | +|---|-----------|--------|---------| +| C1 | CI checks passing | | | +| C2 | Version bump | | | +| C3 | Type hints | | | +| C4 | No hardcoded values | | | +| C5 | Import organization | | | +| C6 | Naming conventions | | | +| C7 | No unused code | | | +| C8 | No unjustified new dependencies | | | +| C9 | Proto code freshness | | | + +### D: API & Design +| # | Criterion | Status | Finding | +|---|-----------|--------|---------| +| D1 | API future-proofing | | | +| D2 | Public API hygiene | | | +| D3 | Breaking changes marked | | | +| D4 | Pagination & tenant filtering | | | +| D5 | Telemetry instrumentation | | | + +### E: Tests & Documentation +| # | Criterion | Status | Finding | +|---|-----------|--------|---------| +| E1 | Tests added/updated | | | +| E2 | Documentation quality | | | +| E3 | Module structure compliance | | | + +--- + +### ❌ Blocking Issues +- **[C2]**: <specific finding with file:line> + +### ⚠️ Non-Blocking Suggestions +- **[C6]**: <suggestion> + +### ✅ Things Done Well +- <observation> +``` + +Verdict: any ❌ → **Blocked** · any ⚠️ → **Needs Minor Work** · all ✅/➖ → **Ready to Merge** + +--- + +## Phase 5 (Optional): Post Review + +Ask: "Post as GitHub PR review? (comment / request-changes / approve / skip)" + +```bash +gh pr review <number> --comment --body "<report>" +gh pr review <number> --request-changes --body "<report>" +gh pr review <number> --approve --body "<report>" +``` diff --git a/.claude/skills/scaffold-module/SKILL.md b/.claude/skills/scaffold-module/SKILL.md new file mode 100644 index 0000000..38001b6 --- /dev/null +++ b/.claude/skills/scaffold-module/SKILL.md @@ -0,0 +1,476 @@ +--- +name: scaffold-module +description: Scaffold a new BTP service module for the SAP Cloud SDK for Python. Use when a contributor wants to add a new service integration and needs the standard directory layout, stubs, and telemetry wiring generated automatically. +tools: Bash, Read, Write, Edit +compatibility: uv, local cloud-sdk-python checkout (run from repo root) +--- + +# New Module Scaffold: SAP Cloud SDK for Python + +Generates the full standard module layout with lint-clean stubs, wires telemetry, then runs a self-review pass on the generated files so you start from a clean baseline. + +Run from the root of your `cloud-sdk-python` checkout. + +--- + +## Phase 1: Collect Inputs + +Ask the user for the following. Confirm before writing any files. + +| Variable | What to ask | Example | +|----------|-------------|---------| +| `MODULE_NAME` | "Module directory name (snake_case)?" | `document_management` | +| `SERVICE_NAME` | "Full service display name?" | `Document Management Service` | +| `SHORT` | "Short prefix for class names (e.g. DMS, AGW)?" | `DMS` | +| `DESCRIPTION` | "One-sentence description of what this service does?" | `Provides document storage and retrieval on SAP BTP.` | + +Derive automatically: +- `SHORT_LOWER` = `SHORT` lowercased (e.g., `dms`) +- `MODULE_UPPER` = `MODULE_NAME` uppercased (e.g., `DOCUMENT_MANAGEMENT`) + +--- + +## Phase 2: Check for Existing Files + +```bash +ls src/sap_cloud_sdk/<MODULE_NAME>/ 2>/dev/null && echo "EXISTS" || echo "OK" +ls tests/<MODULE_NAME>/ 2>/dev/null && echo "EXISTS" || echo "OK" +``` + +If either exists, warn and ask whether to overwrite before continuing. + +--- + +## Phase 3: Generate Files + +Write all files below, substituting all `<PLACEHOLDERS>`. + +### `src/sap_cloud_sdk/<MODULE_NAME>/__init__.py` + +```python +"""SAP Cloud SDK for Python - <SERVICE_NAME> module. + +<DESCRIPTION> + +Usage: + from sap_cloud_sdk.<MODULE_NAME> import create_client + + client = create_client() +""" + +from __future__ import annotations + +import logging +from typing import Optional + +from sap_cloud_sdk.<MODULE_NAME>.client import <SHORT>Client +from sap_cloud_sdk.<MODULE_NAME>.config import load_from_env_or_mount, <SHORT>Config +from sap_cloud_sdk.<MODULE_NAME>.exceptions import ( + <SHORT>Error, + ClientCreationError, + ConfigError, + HttpError, +) + +logger = logging.getLogger(__name__) + + +def create_client( + *, + instance: Optional[str] = None, + config: Optional[<SHORT>Config] = None, +) -> <SHORT>Client: + """Create a <SERVICE_NAME> client with automatic cloud configuration. + + Args: + instance: Instance name for secret resolution. Defaults to "default". + config: Optional explicit <SHORT>Config bypassing secret resolution. + + Returns: + <SHORT>Client ready to use. + + Raises: + ClientCreationError: If client creation fails. + """ + try: + binding = config or load_from_env_or_mount(instance) + return <SHORT>Client(binding) + except Exception as e: + raise ClientCreationError(f"failed to create <SHORT_LOWER> client: {e}") from e + + +__all__ = [ + "create_client", + "<SHORT>Client", + "<SHORT>Config", + "<SHORT>Error", + "ClientCreationError", + "ConfigError", + "HttpError", +] +``` + +### `src/sap_cloud_sdk/<MODULE_NAME>/client.py` + +```python +"""<SERVICE_NAME> client. + +Do not instantiate directly: use create_client() from the module root. +""" + +from __future__ import annotations + +from sap_cloud_sdk.<MODULE_NAME>.config import <SHORT>Config + + +class <SHORT>Client: + """Client for the SAP BTP <SERVICE_NAME>. + + Use :func:`sap_cloud_sdk.<MODULE_NAME>.create_client` instead of + instantiating this class directly. + """ + + def __init__(self, config: <SHORT>Config) -> None: + self._config = config + + # TODO: implement service methods. Add to imports when ready: + # from sap_cloud_sdk.core.telemetry import Module, Operation, record_metrics + # Then decorate each method: + # @record_metrics(Module.<MODULE_UPPER>, Operation.<OPERATION_CONSTANT>) + # def my_operation(self, ...) -> ...: + # ... +``` + +### `src/sap_cloud_sdk/<MODULE_NAME>/config.py` + +```python +"""Configuration and secret resolution for <SERVICE_NAME>.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional + +from sap_cloud_sdk.core.secret_resolver.resolver import ( + read_from_mount_and_fallback_to_env_var, +) +from sap_cloud_sdk.<MODULE_NAME>.exceptions import ConfigError + + +@dataclass +class <SHORT>Config: + """Service binding for <SERVICE_NAME>. + + Attributes: + url: Service base URL. + client_id: OAuth2 client ID. + client_secret: OAuth2 client secret. + token_url: OAuth2 token endpoint URL. + """ + + url: str + client_id: str + client_secret: str + token_url: str + + +@dataclass +class BindingData: + """Raw binding fields read by the secret resolver.""" + + url: str = "" + clientid: str = "" + clientsecret: str = "" + + def validate(self) -> None: + """Validate that required fields are present.""" + if not self.url: + raise ValueError("url is required") + if not self.clientid: + raise ValueError("clientid is required") + if not self.clientsecret: + raise ValueError("clientsecret is required") + + def to_config(self) -> <SHORT>Config: + """Transform raw binding into a unified <SHORT>Config.""" + # TODO: BTP service bindings vary — check the actual binding schema. + # Some services provide a separate UAA URL (e.g. binding["uaa"]["url"]); + # others include it as a top-level "url" field. Do not derive token_url + # from the service URL; read it from the binding instead. + return <SHORT>Config( + url=self.url, + client_id=self.clientid, + client_secret=self.clientsecret, + token_url="", # TODO: populate from the correct binding field + ) + + +def load_from_env_or_mount(instance: Optional[str] = None) -> <SHORT>Config: + """Load <SERVICE_NAME> configuration from mount or environment variables. + + Mount path: /etc/secrets/appfnd/<MODULE_NAME>/{instance}/ + Env fallback: CLOUD_SDK_CFG_<MODULE_UPPER>_{INSTANCE}_{FIELD_KEY} + + Args: + instance: Logical instance name. Defaults to "default". + + Returns: + <SHORT>Config + + Raises: + ConfigError: If loading or validation fails. + """ + inst = instance or "default" + binding = BindingData() + + try: + read_from_mount_and_fallback_to_env_var( + base_volume_mount="/etc/secrets/appfnd", + base_var_name="CLOUD_SDK_CFG", + module="<MODULE_NAME>", + instance=inst, + target=binding, + ) + binding.validate() + return binding.to_config() + except Exception as e: + raise ConfigError( + f"failed to load <MODULE_NAME> configuration for instance='{inst}': {e}" + ) from e +``` + +### `src/sap_cloud_sdk/<MODULE_NAME>/exceptions.py` + +```python +"""Exception classes for the <SERVICE_NAME> module.""" + + +class <SHORT>Error(Exception): + """Base exception for all <SERVICE_NAME> module errors.""" + + +class ClientCreationError(<SHORT>Error): + """Raised when client creation fails.""" + + +class ConfigError(<SHORT>Error): + """Raised when configuration or secret resolution fails.""" + + +class HttpError(<SHORT>Error): + """Raised for HTTP-related errors from <SERVICE_NAME>. + + Attributes: + status_code: HTTP status code, if available. + response_text: Raw response payload for diagnostics, if available. + """ + + def __init__( + self, + message: str, + status_code: int | None = None, + response_text: str | None = None, + ) -> None: + super().__init__(message) + self.status_code = status_code + self.response_text = response_text +``` + +### `src/sap_cloud_sdk/<MODULE_NAME>/_models.py` + +```python +"""Pydantic models for <SERVICE_NAME> API requests and responses.""" + +from __future__ import annotations + + +# TODO: define Pydantic models for service API responses. Add to imports when ready: +# from pydantic import BaseModel +# Example: +# +# class <SHORT>Item(BaseModel): +# id: str +# name: str +``` + +### `src/sap_cloud_sdk/<MODULE_NAME>/py.typed` + +Empty file (PEP 561 marker). + +### `src/sap_cloud_sdk/<MODULE_NAME>/user-guide.md` + +```markdown +# <SERVICE_NAME>: User Guide + +<DESCRIPTION> + +## Prerequisites + +Add the service binding to your `app.yaml`: + +```yaml +requires: + - name: my-<SHORT_LOWER>-instance + service: <SHORT_LOWER> # TODO: replace with the actual BTP service name + plan: standard +``` + +## Installation + +```bash +uv add sap-cloud-sdk +``` + +## Quick Start + +```python +from sap_cloud_sdk.<MODULE_NAME> import create_client + +client = create_client() + +# TODO: add the most common usage example +``` + +## Configuration + +Credentials are resolved automatically from: +- **Cloud (mounted secret)**: `/etc/secrets/appfnd/<MODULE_NAME>/{instance}/` +- **Env fallback**: `CLOUD_SDK_CFG_<MODULE_UPPER>_{INSTANCE}_{FIELD}` + +To target a specific binding instance: +```python +client = create_client(instance="my-<SHORT_LOWER>-instance") +``` + +## API Reference + +### `create_client(*, instance=None, config=None) → <SHORT>Client` + +Creates and returns a configured `<SHORT>Client`. + +<!-- TODO: document each public client method --> + +## Error Handling + +```python +from sap_cloud_sdk.<MODULE_NAME>.exceptions import <SHORT>Error, ConfigError, HttpError + +try: + result = client.my_operation() +except HttpError as e: + print(f"HTTP {e.status_code}: {e}") +except ConfigError as e: + print(f"Config error: {e}") +except <SHORT>Error as e: + print(f"<SHORT_LOWER> error: {e}") +``` +``` + +### `tests/<MODULE_NAME>/__init__.py` + +Empty file. + +### `tests/<MODULE_NAME>/unit/__init__.py` + +Empty file. + +### `tests/<MODULE_NAME>/unit/test_client.py` + +```python +"""Unit tests for <SHORT>Client.""" + +from unittest.mock import MagicMock + +import pytest + +from sap_cloud_sdk.<MODULE_NAME> import create_client +from sap_cloud_sdk.<MODULE_NAME>.client import <SHORT>Client +from sap_cloud_sdk.<MODULE_NAME>.config import <SHORT>Config + + +@pytest.fixture +def mock_config() -> <SHORT>Config: + return <SHORT>Config( + url="https://example.ondemand.com", + client_id="test-client-id", + client_secret="test-client-secret", + token_url="https://example.authentication.eu10.hana.ondemand.com/oauth/token", + ) + + +def test_create_client_returns_client(mock_config: <SHORT>Config) -> None: + client = create_client(config=mock_config) + assert isinstance(client, <SHORT>Client) + + +# TODO: add tests following test_<functionality>_<condition>_<expected_result> +``` + +--- + +## Phase 4: Update Telemetry Module Registry + +Read `src/sap_cloud_sdk/core/telemetry/module.py` and insert the new entry into the `Module` enum in **alphabetical order by key**: + +```python +<MODULE_UPPER> = "<MODULE_NAME>" +``` + +--- + +## Phase 5: Self-Review + +Check the generated files against these criteria. Fix any issues found before reporting to the user. + +**E3: Module structure**: verify all required files exist: +- `__init__.py`, `client.py`, `config.py`, `exceptions.py`, `_models.py`, `py.typed`, `user-guide.md` +- `tests/<MODULE_NAME>/unit/__init__.py`, `tests/<MODULE_NAME>/unit/test_client.py` + +**D5: Telemetry**: verify `Module.<MODULE_UPPER>` was added to `core/telemetry/module.py` in alphabetical order. + +**C3: Type hints**: verify all public functions and the `__init__` method in `client.py` have full type annotations. + +**C6: Naming**: verify class names use the `<SHORT>` prefix consistently, private internal fields use `_` prefix. + +**D2: Public API hygiene**: verify `__all__` in `__init__.py` contains exactly the public symbols and nothing internal. + +Run the full local quality gate on the generated files: +```bash +uv run ruff check src/sap_cloud_sdk/<MODULE_NAME>/ +uv run ruff format --check src/sap_cloud_sdk/<MODULE_NAME>/ +uv run ty check src/sap_cloud_sdk/<MODULE_NAME>/ +uv run pytest tests/<MODULE_NAME>/ -v +``` + +Fix any reported issues before proceeding. + +--- + +## Phase 6: Report + +``` +✅ Generated files: + src/sap_cloud_sdk/<MODULE_NAME>/__init__.py + src/sap_cloud_sdk/<MODULE_NAME>/client.py + src/sap_cloud_sdk/<MODULE_NAME>/config.py + src/sap_cloud_sdk/<MODULE_NAME>/exceptions.py + src/sap_cloud_sdk/<MODULE_NAME>/_models.py + src/sap_cloud_sdk/<MODULE_NAME>/py.typed + src/sap_cloud_sdk/<MODULE_NAME>/user-guide.md + tests/<MODULE_NAME>/__init__.py + tests/<MODULE_NAME>/unit/__init__.py + tests/<MODULE_NAME>/unit/test_client.py +✅ Added Module.<MODULE_UPPER> to core/telemetry/module.py +✅ Self-review passed (or: fixed N issues) + +📋 Your remaining steps: + 1. Add operation constants → src/sap_cloud_sdk/core/telemetry/operation.py + 2. Implement client methods → src/sap_cloud_sdk/<MODULE_NAME>/client.py + Each method needs: @record_metrics(Module.<MODULE_UPPER>, Operation.<YOUR_OP>) + 3. Define response models → src/sap_cloud_sdk/<MODULE_NAME>/_models.py + 4. Add service binding to app.yaml (check BTP catalog for exact service name) + 5. Bump version in pyproject.toml (CI will block without this) + 6. Run full checks: + uv run pytest tests/<MODULE_NAME>/ -v + uv run ty check src/sap_cloud_sdk/<MODULE_NAME>/ +```