Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-21 08:46:24

0001 """Small synchronous client for the corun-ai REST API."""
0002 
0003 import json
0004 import logging
0005 from urllib.error import HTTPError, URLError
0006 from urllib.parse import urlencode
0007 from urllib.request import Request, urlopen
0008 
0009 from decouple import config
0010 
0011 logger = logging.getLogger(__name__)
0012 
0013 
0014 class CorunAPIError(RuntimeError):
0015     """Raised when corun-ai cannot service a request."""
0016 
0017     def __init__(self, message, *, status=None, payload=None):
0018         super().__init__(message)
0019         self.status = status
0020         self.payload = payload
0021 
0022 
0023 def corun_base_url():
0024     from monitor_app.models import EXTERNAL_FACE_DEFAULT
0025     return str(config('CORUN_BASE_URL',
0026                       default=EXTERNAL_FACE_DEFAULT + '/doc') or '').rstrip('/')
0027 
0028 
0029 def corun_api_token():
0030     return str(config('CORUN_API_TOKEN', default='') or '').strip()
0031 
0032 
0033 def corun_configured():
0034     return bool(corun_base_url() and corun_api_token())
0035 
0036 
0037 class CorunClient:
0038     """Token-authenticated corun-ai API client."""
0039 
0040     def __init__(self, *, base_url=None, token=None, timeout=15):
0041         self.base_url = (base_url or corun_base_url()).rstrip('/')
0042         self.token = token if token is not None else corun_api_token()
0043         self.timeout = timeout
0044 
0045     def _request(self, method, path, *, payload=None, query=None):
0046         if not self.base_url:
0047             raise CorunAPIError('CORUN_BASE_URL is not configured')
0048         if not self.token:
0049             raise CorunAPIError('CORUN_API_TOKEN is not configured')
0050 
0051         url = f"{self.base_url}/api/v1/{path.lstrip('/')}"
0052         if query:
0053             clean_query = {
0054                 key: value for key, value in query.items()
0055                 if value not in (None, '', [])
0056             }
0057             if clean_query:
0058                 url = f"{url}?{urlencode(clean_query, doseq=True)}"
0059 
0060         body = None
0061         headers = {
0062             'Authorization': f'Token {self.token}',
0063             'Accept': 'application/json',
0064             'User-Agent': 'swf-monitor-corun-client/1.0',
0065         }
0066         if payload is not None:
0067             body = json.dumps(payload).encode('utf-8')
0068             headers['Content-Type'] = 'application/json'
0069 
0070         req = Request(url, data=body, method=method.upper(), headers=headers)
0071         try:
0072             with urlopen(req, timeout=self.timeout) as resp:
0073                 raw = resp.read().decode('utf-8')
0074         except HTTPError as exc:
0075             raw = exc.read().decode('utf-8', errors='replace')
0076             parsed = _parse_json(raw)
0077             detail = parsed if parsed is not None else raw
0078             raise CorunAPIError(
0079                 f'corun-ai API returned HTTP {exc.code}: {detail}',
0080                 status=exc.code,
0081                 payload=parsed,
0082             ) from exc
0083         except URLError as exc:
0084             raise CorunAPIError(f'corun-ai API request failed: {exc.reason}') from exc
0085 
0086         parsed = _parse_json(raw)
0087         if parsed is None:
0088             raise CorunAPIError('corun-ai API returned non-JSON response')
0089         return parsed
0090 
0091     def create_page(self, *, section, content, title='', status='published',
0092                     data=None, tags=None):
0093         return self._request('POST', 'pages/', payload={
0094             'section': section,
0095             'content': content,
0096             'title': title,
0097             'status': status,
0098             'data': data or {},
0099             'tags': tags or [],
0100         })
0101 
0102     def create_version(self, group_id, *, content, title='', status='draft',
0103                        data=None, tags=None):
0104         """New version of an existing page — corun-ai's correction path.
0105 
0106         corun inherits the prior version's data/title and merges supplied
0107         keys (explicit null removes); omit what you don't mean to change —
0108         an empty value sent explicitly would overwrite inherited metadata.
0109         """
0110         payload = {'content': content, 'status': status}
0111         if title:
0112             payload['title'] = title
0113         if data is not None:
0114             payload['data'] = data
0115         if tags is not None:
0116             payload['tags'] = tags
0117         return self._request('POST', f'pages/{group_id}/versions/', payload=payload)
0118 
0119     def list_versions(self, group_id):
0120         return self._request('GET', f'pages/{group_id}/versions/')
0121 
0122     def get_page(self, group_id):
0123         return self._request('GET', f'pages/{group_id}/')
0124 
0125     def list_pages(self, **filters):
0126         return self._request('GET', 'pages/', query=filters)
0127 
0128     def list_all_pages(self, **filters):
0129         """Return every matching Page across the corun paginated API."""
0130         items = []
0131         offset = 0
0132         while True:
0133             payload = self.list_pages(
0134                 **filters,
0135                 limit=500,
0136                 offset=offset,
0137             )
0138             batch = payload.get('items', []) if isinstance(payload, dict) else payload
0139             batch = batch if isinstance(batch, list) else []
0140             items.extend(batch)
0141             count = payload.get('count') if isinstance(payload, dict) else None
0142             if not batch or (isinstance(count, int) and len(items) >= count):
0143                 return items
0144             offset += len(batch)
0145 
0146     def list_comments(self, group_id):
0147         return self._request('GET', f'pages/{group_id}/comments/')
0148 
0149     def create_comment(self, group_id, *, content, data=None):
0150         return self._request('POST', f'pages/{group_id}/comments/', payload={
0151             'content': content,
0152             'data': data or {},
0153         })
0154 
0155 
0156 def _parse_json(raw):
0157     if raw is None:
0158         return None
0159     raw = raw.strip()
0160     if not raw:
0161         return {}
0162     try:
0163         return json.loads(raw)
0164     except json.JSONDecodeError:
0165         logger.debug('Non-JSON corun-ai API response: %s', raw[:200])
0166         return None