mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 13:24:54 +01:00
447 lines
13 KiB
Python
447 lines
13 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
import logging
|
|
import pymongo
|
|
import certifi
|
|
|
|
from bson.json_util import (
|
|
loads,
|
|
dumps,
|
|
CANONICAL_JSON_OPTIONS
|
|
)
|
|
|
|
from openpype import AYON_SERVER_ENABLED
|
|
if sys.version_info[0] == 2:
|
|
from urlparse import urlparse, parse_qs
|
|
else:
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
|
|
class MongoEnvNotSet(Exception):
|
|
pass
|
|
|
|
|
|
def documents_to_json(docs):
|
|
"""Convert documents to json string.
|
|
|
|
Args:
|
|
Union[list[dict[str, Any]], dict[str, Any]]: Document/s to convert to
|
|
json string.
|
|
|
|
Returns:
|
|
str: Json string with mongo documents.
|
|
"""
|
|
|
|
return dumps(docs, json_options=CANONICAL_JSON_OPTIONS)
|
|
|
|
|
|
def load_json_file(filepath):
|
|
"""Load mongo documents from a json file.
|
|
|
|
Args:
|
|
filepath (str): Path to a json file.
|
|
|
|
Returns:
|
|
Union[dict[str, Any], list[dict[str, Any]]]: Loaded content from a
|
|
json file.
|
|
"""
|
|
|
|
if not os.path.exists(filepath):
|
|
raise ValueError("Path {} was not found".format(filepath))
|
|
|
|
with open(filepath, "r") as stream:
|
|
content = stream.read()
|
|
return loads("".join(content))
|
|
|
|
|
|
def get_project_database_name():
|
|
"""Name of database name where projects are available.
|
|
|
|
Returns:
|
|
str: Name of database name where projects are.
|
|
"""
|
|
|
|
return os.environ.get("AVALON_DB") or "avalon"
|
|
|
|
|
|
def _decompose_url(url):
|
|
"""Decompose mongo url to basic components.
|
|
|
|
Used for creation of MongoHandler which expect mongo url components as
|
|
separated kwargs. Components are at the end not used as we're setting
|
|
connection directly this is just a dumb components for MongoHandler
|
|
validation pass.
|
|
"""
|
|
|
|
# Use first url from passed url
|
|
# - this is because it is possible to pass multiple urls for multiple
|
|
# replica sets which would crash on urlparse otherwise
|
|
# - please don't use comma in username of password
|
|
url = url.split(",")[0]
|
|
components = {
|
|
"scheme": None,
|
|
"host": None,
|
|
"port": None,
|
|
"username": None,
|
|
"password": None,
|
|
"auth_db": None
|
|
}
|
|
|
|
result = urlparse(url)
|
|
if result.scheme is None:
|
|
_url = "mongodb://{}".format(url)
|
|
result = urlparse(_url)
|
|
|
|
components["scheme"] = result.scheme
|
|
components["host"] = result.hostname
|
|
try:
|
|
components["port"] = result.port
|
|
except ValueError:
|
|
raise RuntimeError("invalid port specified")
|
|
components["username"] = result.username
|
|
components["password"] = result.password
|
|
|
|
try:
|
|
components["auth_db"] = parse_qs(result.query)['authSource'][0]
|
|
except KeyError:
|
|
# no auth db provided, mongo will use the one we are connecting to
|
|
pass
|
|
|
|
return components
|
|
|
|
|
|
def get_default_components():
|
|
mongo_url = os.environ.get("OPENPYPE_MONGO")
|
|
if mongo_url is None:
|
|
raise MongoEnvNotSet(
|
|
"URL for Mongo logging connection is not set."
|
|
)
|
|
return _decompose_url(mongo_url)
|
|
|
|
|
|
def should_add_certificate_path_to_mongo_url(mongo_url):
|
|
"""Check if should add ca certificate to mongo url.
|
|
|
|
Since 30.9.2021 cloud mongo requires newer certificates that are not
|
|
available on most of workstation. This adds path to certifi certificate
|
|
which is valid for it. To add the certificate path url must have scheme
|
|
'mongodb+srv' or has 'ssl=true' or 'tls=true' in url query.
|
|
"""
|
|
|
|
parsed = urlparse(mongo_url)
|
|
query = parse_qs(parsed.query)
|
|
lowered_query_keys = set(key.lower() for key in query.keys())
|
|
add_certificate = False
|
|
# Check if url 'ssl' or 'tls' are set to 'true'
|
|
for key in ("ssl", "tls"):
|
|
if key in query and "true" in query[key]:
|
|
add_certificate = True
|
|
break
|
|
|
|
# Check if url contains 'mongodb+srv'
|
|
if not add_certificate and parsed.scheme == "mongodb+srv":
|
|
add_certificate = True
|
|
|
|
# Check if url does already contain certificate path
|
|
if add_certificate and "tlscafile" in lowered_query_keys:
|
|
add_certificate = False
|
|
|
|
return add_certificate
|
|
|
|
|
|
def validate_mongo_connection(mongo_uri):
|
|
"""Check if provided mongodb URL is valid.
|
|
|
|
Args:
|
|
mongo_uri (str): URL to validate.
|
|
|
|
Raises:
|
|
ValueError: When port in mongo uri is not valid.
|
|
pymongo.errors.InvalidURI: If passed mongo is invalid.
|
|
pymongo.errors.ServerSelectionTimeoutError: If connection timeout
|
|
passed so probably couldn't connect to mongo server.
|
|
|
|
"""
|
|
|
|
client = OpenPypeMongoConnection.create_connection(
|
|
mongo_uri, retry_attempts=1
|
|
)
|
|
client.close()
|
|
|
|
|
|
class OpenPypeMongoConnection:
|
|
"""Singleton MongoDB connection.
|
|
|
|
Keeps MongoDB connections by url.
|
|
"""
|
|
|
|
mongo_clients = {}
|
|
log = logging.getLogger("OpenPypeMongoConnection")
|
|
|
|
@staticmethod
|
|
def get_default_mongo_url():
|
|
return os.environ["OPENPYPE_MONGO"]
|
|
|
|
@classmethod
|
|
def get_mongo_client(cls, mongo_url=None):
|
|
if mongo_url is None:
|
|
mongo_url = cls.get_default_mongo_url()
|
|
|
|
connection = cls.mongo_clients.get(mongo_url)
|
|
if connection:
|
|
# Naive validation of existing connection
|
|
try:
|
|
connection.server_info()
|
|
with connection.start_session():
|
|
pass
|
|
except Exception:
|
|
connection = None
|
|
|
|
if not connection:
|
|
cls.log.debug("Creating mongo connection to {}".format(mongo_url))
|
|
connection = cls.create_connection(mongo_url)
|
|
cls.mongo_clients[mongo_url] = connection
|
|
|
|
return connection
|
|
|
|
@classmethod
|
|
def create_connection(cls, mongo_url, timeout=None, retry_attempts=None):
|
|
if AYON_SERVER_ENABLED:
|
|
raise RuntimeError("Created mongo connection in AYON mode")
|
|
parsed = urlparse(mongo_url)
|
|
# Force validation of scheme
|
|
if parsed.scheme not in ["mongodb", "mongodb+srv"]:
|
|
raise pymongo.errors.InvalidURI((
|
|
"Invalid URI scheme:"
|
|
" URI must begin with 'mongodb://' or 'mongodb+srv://'"
|
|
))
|
|
|
|
if timeout is None:
|
|
timeout = int(os.environ.get("AVALON_TIMEOUT") or 1000)
|
|
|
|
kwargs = {
|
|
"serverSelectionTimeoutMS": timeout
|
|
}
|
|
if should_add_certificate_path_to_mongo_url(mongo_url):
|
|
kwargs["tlsCAFile"] = certifi.where()
|
|
|
|
mongo_client = pymongo.MongoClient(mongo_url, **kwargs)
|
|
|
|
if retry_attempts is None:
|
|
retry_attempts = 3
|
|
|
|
elif not retry_attempts:
|
|
retry_attempts = 1
|
|
|
|
last_exc = None
|
|
valid = False
|
|
t1 = time.time()
|
|
for attempt in range(1, retry_attempts + 1):
|
|
try:
|
|
mongo_client.server_info()
|
|
with mongo_client.start_session():
|
|
pass
|
|
valid = True
|
|
break
|
|
|
|
except Exception as exc:
|
|
last_exc = exc
|
|
if attempt < retry_attempts:
|
|
cls.log.warning(
|
|
"Attempt {} failed. Retrying... ".format(attempt)
|
|
)
|
|
time.sleep(1)
|
|
|
|
if not valid:
|
|
raise last_exc
|
|
|
|
cls.log.info("Connected to {}, delay {:.3f}s".format(
|
|
mongo_url, time.time() - t1
|
|
))
|
|
return mongo_client
|
|
|
|
|
|
# ------ Helper Mongo functions ------
|
|
# Functions can be helpful with custom tools to backup/restore mongo state.
|
|
# Not meant as API functionality that should be used in production codebase!
|
|
def get_collection_documents(database_name, collection_name, as_json=False):
|
|
"""Query all documents from a collection.
|
|
|
|
Args:
|
|
database_name (str): Name of database where to look for collection.
|
|
collection_name (str): Name of collection where to look for collection.
|
|
as_json (Optional[bool]): Output should be a json string.
|
|
Default: 'False'
|
|
|
|
Returns:
|
|
Union[list[dict[str, Any]], str]: Queried documents.
|
|
"""
|
|
|
|
client = OpenPypeMongoConnection.get_mongo_client()
|
|
output = list(client[database_name][collection_name].find({}))
|
|
if as_json:
|
|
output = documents_to_json(output)
|
|
return output
|
|
|
|
|
|
def store_collection(filepath, database_name, collection_name):
|
|
"""Store collection documents to a json file.
|
|
|
|
Args:
|
|
filepath (str): Path to a json file where documents will be stored.
|
|
database_name (str): Name of database where to look for collection.
|
|
collection_name (str): Name of collection to store.
|
|
"""
|
|
|
|
# Make sure directory for output file exists
|
|
dirpath = os.path.dirname(filepath)
|
|
if not os.path.isdir(dirpath):
|
|
os.makedirs(dirpath)
|
|
|
|
content = get_collection_documents(database_name, collection_name, True)
|
|
with open(filepath, "w") as stream:
|
|
stream.write(content)
|
|
|
|
|
|
def replace_collection_documents(docs, database_name, collection_name):
|
|
"""Replace all documents in a collection with passed documents.
|
|
|
|
Warnings:
|
|
All existing documents in collection will be removed if there are any.
|
|
|
|
Args:
|
|
docs (list[dict[str, Any]]): New documents.
|
|
database_name (str): Name of database where to look for collection.
|
|
collection_name (str): Name of collection where new documents are
|
|
uploaded.
|
|
"""
|
|
|
|
client = OpenPypeMongoConnection.get_mongo_client()
|
|
database = client[database_name]
|
|
if collection_name in database.list_collection_names():
|
|
database.drop_collection(collection_name)
|
|
col = database[collection_name]
|
|
col.insert_many(docs)
|
|
|
|
|
|
def restore_collection(filepath, database_name, collection_name):
|
|
"""Restore/replace collection from a json filepath.
|
|
|
|
Warnings:
|
|
All existing documents in collection will be removed if there are any.
|
|
|
|
Args:
|
|
filepath (str): Path to a json with documents.
|
|
database_name (str): Name of database where to look for collection.
|
|
collection_name (str): Name of collection where new documents are
|
|
uploaded.
|
|
"""
|
|
|
|
docs = load_json_file(filepath)
|
|
replace_collection_documents(docs, database_name, collection_name)
|
|
|
|
|
|
def get_project_database(database_name=None):
|
|
"""Database object where project collections are.
|
|
|
|
Args:
|
|
database_name (Optional[str]): Custom name of database.
|
|
|
|
Returns:
|
|
pymongo.database.Database: Collection related to passed project.
|
|
"""
|
|
|
|
if not database_name:
|
|
database_name = get_project_database_name()
|
|
return OpenPypeMongoConnection.get_mongo_client()[database_name]
|
|
|
|
|
|
def get_project_connection(project_name, database_name=None):
|
|
"""Direct access to mongo collection.
|
|
|
|
We're trying to avoid using direct access to mongo. This should be used
|
|
only for Create, Update and Remove operations until there are implemented
|
|
api calls for that.
|
|
|
|
Args:
|
|
project_name (str): Project name for which collection should be
|
|
returned.
|
|
database_name (Optional[str]): Custom name of database.
|
|
|
|
Returns:
|
|
pymongo.collection.Collection: Collection related to passed project.
|
|
"""
|
|
|
|
if not project_name:
|
|
raise ValueError("Invalid project name {}".format(str(project_name)))
|
|
return get_project_database(database_name)[project_name]
|
|
|
|
|
|
def get_project_documents(project_name, database_name=None):
|
|
"""Query all documents from project collection.
|
|
|
|
Args:
|
|
project_name (str): Name of project.
|
|
database_name (Optional[str]): Name of mongo database where to look for
|
|
project.
|
|
|
|
Returns:
|
|
list[dict[str, Any]]: Documents in project collection.
|
|
"""
|
|
|
|
if not database_name:
|
|
database_name = get_project_database_name()
|
|
return get_collection_documents(database_name, project_name)
|
|
|
|
|
|
def store_project_documents(project_name, filepath, database_name=None):
|
|
"""Store project documents to a file as json string.
|
|
|
|
Args:
|
|
project_name (str): Name of project to store.
|
|
filepath (str): Path to a json file where output will be stored.
|
|
database_name (Optional[str]): Name of mongo database where to look for
|
|
project.
|
|
"""
|
|
|
|
if not database_name:
|
|
database_name = get_project_database_name()
|
|
|
|
store_collection(filepath, database_name, project_name)
|
|
|
|
|
|
def replace_project_documents(project_name, docs, database_name=None):
|
|
"""Replace documents in mongo with passed documents.
|
|
|
|
Warnings:
|
|
Existing project collection is removed if exists in mongo.
|
|
|
|
Args:
|
|
project_name (str): Name of project.
|
|
docs (list[dict[str, Any]]): Documents to restore.
|
|
database_name (Optional[str]): Name of mongo database where project
|
|
collection will be created.
|
|
"""
|
|
|
|
if not database_name:
|
|
database_name = get_project_database_name()
|
|
replace_collection_documents(docs, database_name, project_name)
|
|
|
|
|
|
def restore_project_documents(project_name, filepath, database_name=None):
|
|
"""Replace documents in mongo with passed documents.
|
|
|
|
Warnings:
|
|
Existing project collection is removed if exists in mongo.
|
|
|
|
Args:
|
|
project_name (str): Name of project.
|
|
filepath (str): File to json file with project documents.
|
|
database_name (Optional[str]): Name of mongo database where project
|
|
collection will be created.
|
|
"""
|
|
|
|
if not database_name:
|
|
database_name = get_project_database_name()
|
|
restore_collection(filepath, database_name, project_name)
|