# Python 3. Standard library, no external packages. # Run on your server with BLOOMHUB_API_KEY in its environment. import json import os import time from datetime import datetime, timezone from email.utils import parsedate_to_datetime from urllib.error import HTTPError, URLError from urllib.parse import urlencode from urllib.request import Request, urlopen BASE = 'https://bloomhub.ru/api/v1' def retry_delay(header, attempt): if header: try: return max(1, float(header)) except ValueError: try: return max(1, (parsedate_to_datetime(header) - datetime.now(timezone.utc)).total_seconds()) except (ValueError, TypeError): pass return 0.5 * 2 ** attempt def api_get(path, query=None): if path not in ['/me', '/locations', '/products', '/recipes', '/stock', '/orders']: raise ValueError('Use a documented list method in this example.') key = os.environ['BLOOMHUB_API_KEY'] query = {k: v for k, v in (query or {}).items() if v is not None and v != ''} url = BASE + path + ('?' + urlencode(query) if query else '') for attempt in range(5): request = Request(url, headers={'Authorization': 'Bearer ' + key, 'Accept': 'application/json'}) try: with urlopen(request, timeout=15) as response: return json.load(response) except HTTPError as error: if (error.code == 429 or error.code >= 500) and attempt < 4: delay = retry_delay(error.headers.get('Retry-After'), attempt) error.close() time.sleep(delay) continue try: body = json.load(error) except (ValueError, OSError): body = {} finally: error.close() detail = body.get('error') code = detail.get('code') if isinstance(detail, dict) else body.get('code') raise RuntimeError('BloomHub HTTP %s: %s' % (error.code, code or 'REQUEST_FAILED')) from None except (URLError, TimeoutError): if attempt == 4: raise time.sleep(retry_delay(None, attempt)) raise RuntimeError('No response after retry limit.') def api_pages(path, query=None): if path not in ['/products', '/recipes', '/stock', '/orders']: raise ValueError('This method does not use pagination.') for page in range(1, 10001): data = api_get(path, {**(query or {}), 'page': page, 'limit': 100}) if not isinstance(data.get('items'), list) or not isinstance(data.get('pagination', {}).get('hasMore'), bool): raise RuntimeError('Unexpected response. Do not advance the synchronization watermark.') yield from data['items'] if not data['pagination']['hasMore']: return raise RuntimeError('Page limit reached. Do not mark a partial sync complete.') if __name__ == '__main__': path = '/' + os.environ.get('BLOOMHUB_RESOURCE', 'products').lstrip('/') params = {'locationId': os.environ.get('BLOOMHUB_LOCATION_ID'), 'updatedAfter': os.environ.get('BLOOMHUB_SINCE')} for item in api_pages(path, params): print(json.dumps(item, ensure_ascii=False))