diff --git a/server/__init__.py b/server/__init__.py index eb8c6b6..b3b5e46 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -4,8 +4,11 @@ from fastapi import Query from nxtools import logging +from ayon_server.exceptions import InvalidSettingsException, AyonException from ayon_server.addons import BaseServerAddon, AddonLibrary +from ayon_server.secrets import Secrets from ayon_server.lib.postgres import Postgres +from ayon_server.api.dependencies import CurrentUser from .settings import ( FtrackSettings, @@ -16,6 +19,7 @@ FTRACK_ID_ATTRIB, FTRACK_PATH_ATTRIB, ) +from .ftrack_session import FtrackSession, InvalidCredentials, ServerError class FtrackAddon(BaseServerAddon): @@ -57,6 +61,11 @@ def initialize(self) -> None: self.get_custom_processor_handlers, method="GET", ) + self.add_endpoint( + "/ftrackProjects", + self.get_ftrack_projects_info, + method="GET", + ) async def get_custom_processor_handlers( self, @@ -108,6 +117,67 @@ async def get_custom_processor_handlers( return output + async def _prepare_ftrack_session( + self, variant: str, settings_model: FtrackSettings | None + ) -> FtrackSession: + # TODO validate user permissions + # - What permissions user must have to allow this endpoint? + if settings_model is None: + settings_model = await self.get_studio_settings(variant) + ftrack_server_url = settings_model.ftrack_server + service_settings = settings_model.service_settings + api_key_secret = service_settings.api_key + username_secret = service_settings.username + + if not ftrack_server_url or not api_key_secret or not username_secret: + raise InvalidSettingsException("Required settings are not set.") + + ftrack_api_key = await Secrets.get(api_key_secret) + ftrack_username = await Secrets.get(username_secret) + + if not ftrack_api_key or not ftrack_username: + raise InvalidSettingsException( + "Invalid service settings, secrets are not set." + ) + + session = FtrackSession( + ftrack_server_url, ftrack_api_key, ftrack_username + ) + try: + await session.validate() + except InvalidCredentials: + raise InvalidSettingsException( + "Service settings contain invalid credentials." + ) + except ServerError as exc: + raise AyonException( + "Unknown error occurred while connecting to ftrack server." + f" {exc}" + ) + return session + + async def get_ftrack_projects_info( + self, + user: CurrentUser, + variant: str = Query("production"), + ) -> {}: + # TODO validate user permissions + # - What permissions user must have to allow this endpoint? + session = await self._prepare_ftrack_session(variant) + projects = [ + { + "id": project["id"], + "name": project["full_name"], + "code": project["name"], + "active": project["status"] == "active", + } + async for project in await session.get_projects() + ] + + return { + "projects": projects, + } + async def _empty_create_ftrack_attributes(self): return False diff --git a/server/ftrack_session.py b/server/ftrack_session.py new file mode 100644 index 0000000..71444ae --- /dev/null +++ b/server/ftrack_session.py @@ -0,0 +1,130 @@ +from typing import Optional + +import httpx +from nxtools import logging + + +class ServerError(Exception): + def __init__(self, message, response=None, error_code=None): + super().__init__(message) + self.response = response + self.error_code = error_code + + @classmethod + def from_call_error(cls, response): + response_data = response.json() + exception = response_data["exception"] + content = response_data["content"] + error_code = response_data.get("error_code") + used_cls = cls + if error_code == "api_credentials_invalid": + return InvalidCredentials( + content, + response, + error_code, + ) + + return used_cls( + f"Server reported error: {exception} ({content})", + response, + error_code, + ) + + +class InvalidCredentials(ServerError): + pass + + +class QueryResult: + def __init__(self, session: "FtrackSession", query: str, limit=500): + self._session = session + self._query = query + self._limit = limit + self._offset = 0 + self._done = False + self._fetched_data = None + + def __aiter__(self): + return self + + async def __anext__(self): + if not self._fetched_data: + if not self._done: + self._fetched_data = await self._fetch_more() + if not self._fetched_data: + self._done = True + raise StopAsyncIteration() + + return self._fetched_data.pop(0) + + async def _fetch_more(self): + if self._done: + return None + + query_parts = [self._query, f"limit {self._limit}"] + if self._offset: + query_parts.append(f"offset {self._offset}") + + result = await self._session.call({ + "action": "query", + "expression": " ".join(query_parts), + }) + self._offset += self._limit + if len(result["data"]) < self._limit: + self._done = True + return result["data"] + + +class FtrackSession: + def __init__(self, server_url: str, api_key: str, username: str): + server_url = server_url.rstrip("/") + self._server_url = server_url + self._api_url = server_url + "/api" + self._client = httpx.AsyncClient( + headers={ + "content-type": "application/json", + "accept": "application/json", + "ftrack-api-key": api_key, + "ftrack-user": username, + } + ) + + async def call(self, data): + single_item = isinstance(data, dict) + if single_item: + data = [data] + try: + response = await self._client.post(self._api_url, json=data) + except httpx.ConnectError as exc: + raise ServerError( + f"Failed to connect to server {exc}", + None, + "connection_error", + ) + + if response.status_code != 200: + raise ServerError(response, response.text) + response_data = response.json() + if "exception" in response_data: + raise ServerError.from_call_error(response) + + if single_item: + return response_data[0] + return response_data + + async def validate(self): + await self.get_server_information() + + async def get_server_information(self): + return await self.call({"action": "query_server_information"}) + + async def query(self, query: str, limit=500): + return QueryResult(self, query, limit) + + async def get_projects( + self, fields: Optional[set[str]] = None + ): + if not fields: + fields = {"id", "full_name", "name", "status"} + fields_str = ", ".join(fields) + return await self.query(f"select {fields_str} from Project")