Login/register pages now correctly unwrap API response {data: {token, user}}.
Cleaned up leftover apiFetch code.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
5.0 KiB
Python
124 lines
5.0 KiB
Python
# Task Team Connector — REST API client (stdlib-only, no extra deps)
|
|
import json
|
|
import logging
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TaskTeamApiClient:
|
|
"""Thin HTTP wrapper for the Task Team REST API."""
|
|
|
|
def __init__(self, base_url: str, api_key: str):
|
|
self.base_url = base_url.rstrip('/')
|
|
self.api_key = api_key # service-account e-mail
|
|
self._token: str | None = None # cached JWT
|
|
|
|
# ------------------------------------------------------------------
|
|
# Auth
|
|
# ------------------------------------------------------------------
|
|
|
|
def _authenticate(self):
|
|
"""Login with the service account and cache the JWT."""
|
|
payload = json.dumps({'email': self.api_key}).encode()
|
|
req = urllib.request.Request(
|
|
f'{self.base_url}/auth/login',
|
|
data=payload,
|
|
headers={'Content-Type': 'application/json'},
|
|
method='POST',
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
data = json.loads(resp.read())
|
|
self._token = data.get('data', {}).get('token')
|
|
except urllib.error.HTTPError as exc:
|
|
_logger.error('Task Team authentication failed: %s', exc)
|
|
self._token = None
|
|
|
|
def _headers(self) -> dict:
|
|
if not self._token:
|
|
self._authenticate()
|
|
hdrs = {'Content-Type': 'application/json'}
|
|
if self._token:
|
|
hdrs['Authorization'] = f'Bearer {self._token}'
|
|
return hdrs
|
|
|
|
# ------------------------------------------------------------------
|
|
# Generic request
|
|
# ------------------------------------------------------------------
|
|
|
|
def _request(self, method: str, path: str, payload=None, *, retry=True):
|
|
url = f'{self.base_url}{path}'
|
|
data = json.dumps(payload).encode() if payload is not None else None
|
|
req = urllib.request.Request(url, data=data, headers=self._headers(), method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
return json.loads(resp.read())
|
|
except urllib.error.HTTPError as exc:
|
|
if exc.code == 401 and retry:
|
|
# Token expired — re-auth once
|
|
self._token = None
|
|
return self._request(method, path, payload, retry=False)
|
|
body = exc.read().decode(errors='replace') if exc else ''
|
|
_logger.error('Task Team API %s %s → %s: %s', method, path, exc.code, body[:200])
|
|
raise
|
|
except Exception as exc:
|
|
_logger.error('Task Team API request error (%s %s): %s', method, path, exc)
|
|
raise
|
|
|
|
# ------------------------------------------------------------------
|
|
# User helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def get_or_create_user(self, email: str, name: str) -> dict:
|
|
"""Find a Task Team user by e-mail, creating them if absent."""
|
|
payload = json.dumps({'email': email}).encode()
|
|
req = urllib.request.Request(
|
|
f'{self.base_url}/auth/login',
|
|
data=payload,
|
|
headers={'Content-Type': 'application/json'},
|
|
method='POST',
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
return json.loads(resp.read())
|
|
except urllib.error.HTTPError as exc:
|
|
if exc.code == 401:
|
|
# Not registered yet
|
|
reg_payload = json.dumps({'email': email, 'name': name}).encode()
|
|
reg_req = urllib.request.Request(
|
|
f'{self.base_url}/auth/register',
|
|
data=reg_payload,
|
|
headers={'Content-Type': 'application/json'},
|
|
method='POST',
|
|
)
|
|
with urllib.request.urlopen(reg_req, timeout=10) as resp:
|
|
return json.loads(resp.read())
|
|
raise
|
|
|
|
# ------------------------------------------------------------------
|
|
# Task CRUD
|
|
# ------------------------------------------------------------------
|
|
|
|
def create_task(self, payload: dict, user_email: str | None = None) -> dict:
|
|
return self._request('POST', '/tasks', payload)
|
|
|
|
def update_task(self, tt_task_id: str, payload: dict, user_email: str | None = None) -> dict:
|
|
return self._request('PUT', f'/tasks/{tt_task_id}', payload)
|
|
|
|
def get_task(self, tt_task_id: str) -> dict:
|
|
return self._request('GET', f'/tasks/{tt_task_id}')
|
|
|
|
def list_tasks(self, **filters) -> dict:
|
|
qs = '&'.join(f'{k}={v}' for k, v in filters.items() if v is not None)
|
|
path = f'/tasks?{qs}' if qs else '/tasks'
|
|
return self._request('GET', path)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Webhook
|
|
# ------------------------------------------------------------------
|
|
|
|
def send_webhook(self, event: str, payload: dict) -> dict:
|
|
return self._request('POST', '/connectors/webhook/odoo', {'event': event, **payload})
|