Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

List ftrack project on AYON server #195

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,6 +19,7 @@
FTRACK_ID_ATTRIB,
FTRACK_PATH_ATTRIB,
)
from .ftrack_session import FtrackSession, InvalidCredentials, ServerError


class FtrackAddon(BaseServerAddon):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
130 changes: 130 additions & 0 deletions server/ftrack_session.py
Original file line number Diff line number Diff line change
@@ -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")
Loading