api_server.py ============= .. module:: api_server The Flask REST server that exposes the Evercycle API over HTTP. Global Configuration -------------------- .. py:data:: ALLOWED_ORIGINS :type: list[str] List of origins permitted to make CORS requests. .. py:data:: token_expiry :type: datetime | None Global variable tracking when the current access token expires. .. py:data:: refresh_token :type: str Global variable holding the refresh token. .. py:data:: token_refresh_lock :type: threading.Lock Reentrant lock used to prevent concurrent token refresh operations. Functions --------- .. py:function:: is_origin_allowed(origin) Check if an origin is in the allowed list or matches a wildcard pattern. :param str origin: The Origin header value. :return: True if allowed, False otherwise. .. py:function:: add_cors_headers(response) Flask ``after_request`` handler that injects CORS headers for allowed origins. :param response: Flask Response object. :return: Modified Response object. .. py:function:: handle_options(path) Handle CORS preflight OPTIONS requests for any path. :param str path: The request path (captured by Flask routing). :return: Empty 200 response with CORS headers. .. py:function:: is_token_valid() Check if the current token is still valid (with a 60-second buffer). :return: True if valid, False otherwise. .. py:function:: refresh_auth_token() Refresh the authentication token if needed. Uses a lock to prevent race conditions. Attempts token refresh via the Evercycle refresh endpoint first. Falls back to re-authentication using ``config/auth-signin.json`` if refresh fails or no refresh token exists. :return: True if a valid token is available, False otherwise. Flask Routes ------------ .. py:function:: health_check() .. http:get:: /health Health check endpoint. Always returns ``{"status": "ok", ...}``. .. py:function:: authenticate() .. http:post:: /api/auth Authenticate with the Evercycle API. Expects JSON body with ``username`` and ``password``. On success, stores tokens globally and in ``auth.properties``. .. py:function:: list_apis() .. http:get:: /api/list List all available APIs from the config directory. .. py:function:: call_api(api_name) .. http:any:: /api/ Dynamic proxy endpoint for any configured Evercycle API. **Special handling per API:** * ``get-asset-id`` (GET or POST): extracts ``id``, monkey-patches ``api_client.call_api`` to inject the asset ID into the URL path. * ``edit-asset-id`` (PUT): extracts ``id`` from body, merges body with defaults from config, overrides path to ``/v1/asset/{id}``. * ``grade`` (PUT): extracts ``id`` and ``grade`` from body, merges remaining fields with defaults, overrides path to ``/v1/pricebook/{id}/{grade}``. * ``create``, ``single-confirm``, ``create-request`` (POST): merges request JSON with default body from config, passes merged JSON to ``call_api``. Implements a retry loop (max 2 attempts). On auth failure (401, token expired, Unauthorized), it forces a token refresh and retries once. :param str api_name: The API config name (e.g., ``get-all``). :return: JSON response with ``status``, ``data`` or ``message``, ``raw_output``, and ``detailed_error`` on failure. .. py:function:: main() Entry point for running the Flask development server from the command line. Parses ``--port``, ``--host``, and ``--debug`` arguments and calls ``app.run()``.