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 APIredis_client: Redis-Connection für Token-Cachingproduct_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:
- Wenn kein Redis oder
force_refresh=True: Fetch new - Wenn cached Token existiert und nicht abgelaufen: Return cached
- 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-Headersparams(dict): Query-Parametersjson_data(dict): JSON-Body für POST/PUTtimeout_seconds(int): Override default timeout
Returns: dict|None - JSON-Response oder None
Logic:
- Get Bearer Token (cached oder fresh)
- Setze Authorization Header
- Async HTTP-Request mit aiohttp
- Bei 401: Refresh Token und retry
- Parse JSON Response
- Return Result
Error Handling:
aiohttp.ClientError: Network/HTTP errors401 Unauthorized: Auto-refresh Token und retry (einmal)Timeout: NachADVOWARE_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
- Generate nonce (UUID4)
- Get current UTC timestamp (ISO format)
- Generate HMAC signature
- POST to
https://security.advo-net.net/api/v1/Token:{ "AppID": "...", "Kanzlei": "...", "Database": "...", "User": "...", "Role": 2, "Product": 64, "Password": "...", "Nonce": "...", "HMAC512Signature": "...", "RequestTimeStamp": "..." } - Extract
access_tokenfrom response - 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