Files
motia/bitbylaw/services/ADVOWARE_SERVICE.md
2026-02-07 09:23:49 +00:00

8.0 KiB

AdvowareAPI Service

Übersicht

Der AdvowareAPI Service ist der zentrale HTTP-Client für alle Kommunikation mit der Advoware REST-API. Er abstrahiert die komplexe HMAC-512 Authentifizierung und bietet ein einfaches Interface für API-Calls.

Location

services/advoware.py

Verwendung

from services.advoware import AdvowareAPI

# In Step-Handler
async def handler(req, context):
    advoware = AdvowareAPI(context)
    result = await advoware.api_call('/employees', method='GET')
    return {'status': 200, 'body': {'result': result}}

Klassen

AdvowareAPI

Constructor: __init__(self, context=None)

  • context: Motia context für Logging (optional)

Attributes:

  • API_BASE_URL: Base URL der Advoware API
  • redis_client: Redis-Connection für Token-Caching
  • product_id, app_id, api_key: Auth-Credentials aus Config

Methoden

get_access_token(force_refresh=False)

Holt Bearer Token aus Redis Cache oder fetcht neuen Token.

Parameters:

  • force_refresh (bool): Cache ignorieren und neuen Token holen

Returns: str - Bearer Token

Logic:

  1. Wenn kein Redis oder force_refresh=True: Fetch new
  2. Wenn cached Token existiert und nicht abgelaufen: Return cached
  3. Sonst: Fetch new und cache

Caching:

  • Key: advoware_access_token
  • TTL: 53 Minuten (55min Lifetime - 2min Safety)
  • Timestamp-Key: advoware_token_timestamp

Example:

api = AdvowareAPI()
token = api.get_access_token()  # From cache
token = api.get_access_token(force_refresh=True)  # Fresh

api_call(endpoint, method='GET', headers=None, params=None, json_data=None, ...)

Führt authentifizierten API-Call zu Advoware aus.

Parameters:

  • endpoint (str): API-Pfad (z.B. /employees)
  • method (str): HTTP-Method (GET, POST, PUT, DELETE)
  • headers (dict): Zusätzliche HTTP-Headers
  • params (dict): Query-Parameters
  • json_data (dict): JSON-Body für POST/PUT
  • timeout_seconds (int): Override default timeout

Returns: dict|None - JSON-Response oder None

Logic:

  1. Get Bearer Token (cached oder fresh)
  2. Setze Authorization Header
  3. Async HTTP-Request mit aiohttp
  4. Bei 401: Refresh Token und retry
  5. Parse JSON Response
  6. Return Result

Error Handling:

  • aiohttp.ClientError: Network/HTTP errors
  • 401 Unauthorized: Auto-refresh Token und retry (einmal)
  • Timeout: Nach ADVOWARE_API_TIMEOUT_SECONDS

Example:

# GET Request
employees = await api.api_call('/employees', method='GET', params={'limit': 10})

# POST Request
new_appt = await api.api_call(
    '/appointments',
    method='POST',
    json_data={'datum': '2026-02-10', 'text': 'Meeting'}
)

# PUT Request
updated = await api.api_call(
    '/appointments/123',
    method='PUT',
    json_data={'text': 'Updated'}
)

# DELETE Request
await api.api_call('/appointments/123', method='DELETE')

Authentifizierung

HMAC-512 Signature

Advoware verwendet HMAC-512 für Request-Signierung:

Message Format:

{product_id}:{app_id}:{nonce}:{timestamp}

Key: Base64-decoded API Key

Hash: SHA512

Output: Base64-encoded Signature

Implementation:

def _generate_hmac(self, request_time_stamp, nonce=None):
    if not nonce:
        nonce = str(uuid.uuid4())
    message = f"{self.product_id}:{self.app_id}:{nonce}:{request_time_stamp}"
    api_key_bytes = base64.b64decode(self.api_key)
    signature = hmac.new(api_key_bytes, message.encode(), hashlib.sha512)
    return base64.b64encode(signature.digest()).decode('utf-8')

Token-Fetch Flow

  1. Generate nonce (UUID4)
  2. Get current UTC timestamp (ISO format)
  3. Generate HMAC signature
  4. POST to https://security.advo-net.net/api/v1/Token:
    {
      "AppID": "...",
      "Kanzlei": "...",
      "Database": "...",
      "User": "...",
      "Role": 2,
      "Product": 64,
      "Password": "...",
      "Nonce": "...",
      "HMAC512Signature": "...",
      "RequestTimeStamp": "..."
    }
    
  5. Extract access_token from response
  6. Cache in Redis (53min TTL)

Redis Usage

Keys

DB 1 (REDIS_DB_ADVOWARE_CACHE):

  • advoware_access_token (string, TTL: 3180s = 53min)
  • advoware_token_timestamp (string, TTL: 3180s)

Operations

# Set Token
self.redis_client.set(
    self.TOKEN_CACHE_KEY,
    access_token,
    ex=(self.token_lifetime_minutes - 2) * 60
)

# Get Token
cached_token = self.redis_client.get(self.TOKEN_CACHE_KEY)
if cached_token:
    return cached_token.decode('utf-8')

Fallback

Wenn Redis nicht erreichbar:

  • Logge Warning
  • Fetche Token bei jedem Request (keine Caching)
  • Funktioniert, aber langsamer

Logging

Log Messages

# Via context.logger (wenn vorhanden)
context.logger.info("Access token fetched successfully")
context.logger.error(f"API call failed: {e}")

# Fallback zu Python logging
logger.info("Connected to Redis for token caching")
logger.debug(f"Token request data: AppID={self.app_id}")

Log Levels

  • DEBUG: Token Details, Request-Parameter
  • INFO: Token-Fetch, API-Calls, Cache-Hits
  • ERROR: Auth-Fehler, API-Fehler, Network-Fehler

Configuration

Environment Variables

# API Settings
ADVOWARE_API_BASE_URL=https://www2.advo-net.net:90/
ADVOWARE_PRODUCT_ID=64
ADVOWARE_APP_ID=your_app_id
ADVOWARE_API_KEY=base64_encoded_hmac_key
ADVOWARE_KANZLEI=your_kanzlei
ADVOWARE_DATABASE=your_database
ADVOWARE_USER=api_user
ADVOWARE_ROLE=2
ADVOWARE_PASSWORD=your_password

# Timeouts
ADVOWARE_TOKEN_LIFETIME_MINUTES=55
ADVOWARE_API_TIMEOUT_SECONDS=30

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB_ADVOWARE_CACHE=1
REDIS_TIMEOUT_SECONDS=5

Error Handling

Exceptions

AdvowareTokenError:

  • Raised when token fetch fails
  • Beispiel: Invalid credentials, HMAC signature mismatch

aiohttp.ClientError:

  • Network errors, HTTP errors (außer 401)
  • Timeouts, Connection refused, etc.

Retry Logic

401 Unauthorized:

  • Automatic retry mit fresh token (einmal)
  • Danach: Exception an Caller

Other Errors:

  • Keine Retry (fail-fast)
  • Exception direkt an Caller

Performance

Response Time

  • With cached token: 200-800ms (Advoware API Latency)
  • With token fetch: +1-2s für Token-Request
  • Timeout: 30s (konfigurierbar)

Caching

  • Hit Rate: >99% (Token cached 53min, API calls häufiger)
  • Miss: Nur bei erstem Call oder Token-Expiry

Testing

Manual Testing

# Test Token Fetch
from services.advoware import AdvowareAPI
api = AdvowareAPI()
token = api.get_access_token(force_refresh=True)
print(f"Token: {token[:20]}...")

# Test API Call
import asyncio
async def test():
    api = AdvowareAPI()
    result = await api.api_call('/employees', params={'limit': 5})
    print(result)

asyncio.run(test())

Unit Testing

from unittest.mock import AsyncMock, MagicMock, patch
import pytest

@pytest.mark.asyncio
async def test_api_call_with_cached_token():
    # Mock Redis
    redis_mock = MagicMock()
    redis_mock.get.return_value = b'cached_token'
    
    # Mock aiohttp
    with patch('aiohttp.ClientSession') as session_mock:
        response_mock = AsyncMock()
        response_mock.status = 200
        response_mock.json = AsyncMock(return_value={'data': 'test'})
        session_mock.return_value.__aenter__.return_value.request.return_value.__aenter__.return_value = response_mock
        
        api = AdvowareAPI()
        api.redis_client = redis_mock
        result = await api.api_call('/test')
        
        assert result == {'data': 'test'}
        redis_mock.get.assert_called_once()

Security

Secrets

  • API Key aus Environment (nicht hardcoded)
  • Password aus Environment
  • Token nur in Redis (localhost)
  • Token nicht in Logs

Best Practices

  • API Key immer Base64-encoded speichern
  • Token nicht länger als 55min cachen
  • Redis localhost-only (keine remote connections)
  • Logs keine credentials enthalten