# -*- coding: utf-8 -*-
import os
import re
import socket
import struct
import json
import requests
import datetime
import iso8601

from .cclib import *
from .dbtypes import *
from random import shuffle
from . import cargador


# PostgreSQL types
TYPES_INT		= {"integer", "serial", "smallint", "int4", "int2", "int8"}
TYPES_FLOAT		= {"numeric", "decimal", "real", "double precision", "float4", "float8"}
TYPES_DATETIME	= {"timestamptz"}

# Main connection server url
DEFAULT_URL = ("https://login1.cerebrohq.com;https://login2.cerebrohq.com;https://login3.cerebrohq.com;https://login4.cerebrohq.com;"
	"https://login1.cerebrohq.com:4443;https://login2.cerebrohq.com:4443;https://login3.cerebrohq.com:4443;https://login4.cerebrohq.com:4443")

# DB -> URL map
URL_MAP = {
	"db.cerebrohq.com:memoria": "https://db1.cerebrohq.com/dapi/rpc.php"
	, "db1.cerebrohq.com:memoria": "https://db1.cerebrohq.com/dapi/rpc.php"
	, "dbsinga.cerebrohq.com:memoria": "https://dbsinga.cerebrohq.com/dapi/rpc.php"
	, "dbsf.cerebrohq.com:memoria": "https://dbsf.cerebrohq.com/dapi/rpc.php"
	, "db2.cerebrohq.com:memoria2": "https://db2.cerebrohq.com/dapi2/rpc.php"
	, "db2.cerebrohq.com:memoria3": "https://db2.cerebrohq.com/dapi3/rpc.php"
	, "db2.cerebrohq.com:memoria4": "https://db2.cerebrohq.com/dapi4/rpc.php"
	, "db2.cerebrohq.com:memoria5": "https://db2.cerebrohq.com/dapi5/rpc.php"
}
# Verify only Cerebro URL's by default
URL_VERIFY = set(URL_MAP.values()) | { "{}/{}/rpc.php".format(url, api) for url in DEFAULT_URL.split(';') for api in ("dapi", "lapi") }

# PostgreSQL error codes that require another try
RECONNECT_ERROR_CODES = {"08000", "08003", "08006", "08001", "08004", "08007", "08P01"}

# Cerebro communications
CEREBRO_PORT = 51051
CEREBRO_PACKET_TYPE = 6
CEREBRO_PROTO_VERSION = 3
CEREBRO_PROTO_HEADER = 0xEEEEFF01
CEREBRO_BUFF_SIZE = 1024

# RegExps
RE_TASK_NAME = r"[\\\/\"':\?|*<>]+"		# r"[\\\\/#:?&'\",|+]+"

def json_serial(obj):
	"""
	JSON serializer for unsupported by default types.
	"""

	if isinstance(obj, (datetime.datetime, datetime.date)):
		return obj.isoformat()
	elif isinstance(obj, set):
		return list(obj)

	raise TypeError("Type %s is not JSON serializable".format(type(obj)))

def to_array(obj):
	"""
	Ensure argument is of array-like type.
	"""

	return obj if isinstance(obj, (set, list, tuple)) else [obj]

def text_unicode(text):
	"""
	Ensure text is not a byte-string.
	"""

	if PY3:
		return text
	else:
		return string_unicode(text)

def format_rpc_url(host):
	"""
	Format connection URL
	"""
	if not host: return ""
	host = host.lower()
	return "{0}{1}{2}".format("" if host.startswith("http") else "https://", host, "" if host.endswith(".php") else "/dapi/rpc.php")

def socket_recvall(sock):
	"""
	Recieve all byte data from open socket
	"""
	fragments = []
	while True:
		chunk = sock.recv(CEREBRO_BUFF_SIZE)
		if chunk: fragments.append(chunk)
		if len(chunk) < CEREBRO_BUFF_SIZE: break
	return b''.join(fragments)


class Database(object):

	__slots__ = ("__db_timeout", "__db_reconn_count", "__is_connected_by_client", "__db_user", "__db_password",
				"__db_long_token", "__sid", "__query_id", "__db_url_primary", "__db_url_secondary", "__db_url_proxy"
				, "__session", "__server_list", "__verify_ssl")

	"""
	:param str db_host: host name.
	:param int db_port: port.
	:param int db_timeout: disconnect timeout (seconds).
	:param int db_reconn_count: reconnect counts.

	The Database class is used for connection to the database,
	it contains a set of methods, executing Cerebro standard queries,
	and enables custom SQL queries.

	::

		# Устанавливаем соединение с базой данных

		if db.connect_from_cerebro_client() != db.CLIENT_CONNECTED: # Пробуем установить соединение с помощью запущенного клиента Cerebro.
			# Если не выходит, устанавливаем соединение с помощью логина и пароля
			db.connect('user', 'password')

	.. note:: В классе существуют функции изменяющие свойства задач, которые могут принимать на вход массив
		идентификаторов. Если необходимо установить нескольким задачам одинаковое значение свойства, предпочитайте
		использовать передачу массива идентификаторов в качестве аргумента вместо использования циклов. Это значительно
		повысит производительность.

	::

		# Использование массивов идентификаторов

		to_do_task_list = db.to_do_task_list(db.current_user_id(), True) # получаем список задач текущего пользователя

		tsks = set()

		for task in to_do_task_list:
			tsks.add(task[dbtypes.TASK_DATA_ID])

		db.task_set_priority(tsks,
			dbtypes.TASK_PRIORITY_ABOVE_NORMAL) # установили сразу нескольким задачам приоритет выше обычного

	.. rubric:: Methods

	* :py:meth:`activities()					<py_cerebro.database.Database.activities>`
	* :py:meth:`add_attachment()				<py_cerebro.database.Database.add_attachment>`
	* :py:meth:`add_client_review()				<py_cerebro.database.Database.add_client_review>`
	* :py:meth:`add_definition()				<py_cerebro.database.Database.add_definition>`
	* :py:meth:`add_note()						<py_cerebro.database.Database.add_note>`
	* :py:meth:`add_report()					<py_cerebro.database.Database.add_report>`
	* :py:meth:`add_resource_report()			<py_cerebro.database.Database.add_resource_report>`
	* :py:meth:`add_review()					<py_cerebro.database.Database.add_review>`
	* :py:meth:`add_task()						<py_cerebro.database.Database.add_task>`
	* :py:meth:`attachment_hashtags()			<py_cerebro.database.Database.attachment_hashtags>`
	* :py:meth:`attachment_remove_hashtags()	<py_cerebro.database.Database.attachment_remove_hashtags>`
	* :py:meth:`attachment_set_hashtags()		<py_cerebro.database.Database.attachment_set_hashtags>`
	* :py:meth:`checks()						<py_cerebro.database.Database.checks>`
	* :py:meth:`connect()						<py_cerebro.database.Database.connect>`
	* :py:meth:`connect_from_cerebro_client()	<py_cerebro.database.Database.connect_from_cerebro_client>`
	* :py:meth:`copy_tasks()					<py_cerebro.database.Database.copy_tasks>`
	* :py:meth:`current_user_id()				<py_cerebro.database.Database.current_user_id>`
	* :py:meth:`current_user()					<py_cerebro.database.Database.current_user>`
	* :py:meth:`drop_link_tasks()				<py_cerebro.database.Database.drop_link_tasks>`
	* :py:meth:`execute()						<py_cerebro.database.Database.execute>`
	* :py:meth:`message()						<py_cerebro.database.Database.message>`
	* :py:meth:`message_attachments()			<py_cerebro.database.Database.message_attachments>`
	* :py:meth:`message_hashtags()				<py_cerebro.database.Database.message_hashtags>`
	* :py:meth:`message_remove_hashtags()		<py_cerebro.database.Database.message_remove_hashtags>`
	* :py:meth:`message_set_hashtags()			<py_cerebro.database.Database.message_set_hashtags>`
	* :py:meth:`messages()						<py_cerebro.database.Database.messages>`
	* :py:meth:`project_tags()					<py_cerebro.database.Database.project_tags>`
	* :py:meth:`root_tasks()					<py_cerebro.database.Database.root_tasks>`
	* :py:meth:`set_link_tasks()				<py_cerebro.database.Database.set_link_tasks>`
	* :py:meth:`statuses()						<py_cerebro.database.Database.statuses>`
	* :py:meth:`tag_enums()						<py_cerebro.database.Database.tag_enums>`
	* :py:meth:`task()							<py_cerebro.database.Database.task>`
	* :py:meth:`task_allocated()				<py_cerebro.database.Database.task_allocated>`
	* :py:meth:`task_attachments()				<py_cerebro.database.Database.task_attachments>`
	* :py:meth:`task_by_url()					<py_cerebro.database.Database.task_by_url>`
	* :py:meth:`task_children()					<py_cerebro.database.Database.task_children>`
	* :py:meth:`task_definition()				<py_cerebro.database.Database.task_definition>`
	* :py:meth:`task_hashtags()					<py_cerebro.database.Database.task_hashtags>`
	* :py:meth:`task_links()					<py_cerebro.database.Database.task_links>`
	* :py:meth:`task_messages()					<py_cerebro.database.Database.task_messages>`
	* :py:meth:`task_possible_statuses()		<py_cerebro.database.Database.task_possible_statuses>`
	* :py:meth:`task_checks()					<py_cerebro.database.Database.task_checks>`
	* :py:meth:`task_user_note()				<py_cerebro.database.Database.task_user_note>`
	* :py:meth:`task_remove_allocated()			<py_cerebro.database.Database.task_remove_allocated>`
	* :py:meth:`task_remove_checks()			<py_cerebro.database.Database.task_remove_checks>`
	* :py:meth:`task_remove_hashtags()			<py_cerebro.database.Database.task_remove_hashtags>`
	* :py:meth:`task_set_activity()				<py_cerebro.database.Database.task_set_activity>`
	* :py:meth:`task_set_allocated()			<py_cerebro.database.Database.task_set_allocated>`
	* :py:meth:`task_set_budget()				<py_cerebro.database.Database.task_set_budget>`
	* :py:meth:`task_set_checks()				<py_cerebro.database.Database.task_set_checks>`
	* :py:meth:`task_set_checks_value()			<py_cerebro.database.Database.task_set_checks_value>`
	* :py:meth:`task_set_finish()				<py_cerebro.database.Database.task_set_finish>`
	* :py:meth:`task_set_flag()					<py_cerebro.database.Database.task_set_flag>`
	* :py:meth:`task_set_hashtags()				<py_cerebro.database.Database.task_set_hashtags>`
	* :py:meth:`task_set_name()					<py_cerebro.database.Database.task_set_name>`
	* :py:meth:`task_set_planned_time()			<py_cerebro.database.Database.task_set_planned_time>`
	* :py:meth:`task_set_priority()				<py_cerebro.database.Database.task_set_priority>`
	* :py:meth:`task_set_progress()				<py_cerebro.database.Database.task_set_progress>`
	* :py:meth:`task_set_start()				<py_cerebro.database.Database.task_set_start>`
	* :py:meth:`task_set_status()				<py_cerebro.database.Database.task_set_status>`
	* :py:meth:`task_set_tag_enum()				<py_cerebro.database.Database.task_set_tag_enum>`
	* :py:meth:`task_set_tag_float()			<py_cerebro.database.Database.task_set_tag_float>`
	* :py:meth:`task_set_tag_int()				<py_cerebro.database.Database.task_set_tag_int>`
	* :py:meth:`task_set_tag_string()			<py_cerebro.database.Database.task_set_tag_string>`
	* :py:meth:`task_set_tag_user()				<py_cerebro.database.Database.task_set_tag_user>`
	* :py:meth:`task_set_user_note()			<py_cerebro.database.Database.task_set_user_note>`
	* :py:meth:`task_tag_enums()				<py_cerebro.database.Database.task_tag_enums>`
	* :py:meth:`task_tag_reset()				<py_cerebro.database.Database.task_tag_reset>`
	* :py:meth:`task_tags()						<py_cerebro.database.Database.task_tags>`
	* :py:meth:`tasks()							<py_cerebro.database.Database.tasks>`
	* :py:meth:`to_do_task_list()				<py_cerebro.database.Database.to_do_task_list>`
	* :py:meth:`users()							<py_cerebro.database.Database.users>`
	"""

	# Client connection status codes
	CLIENT_CONNECTED		= 0
	CLIENT_NOT_CONNECTED	= 1
	CLIENT_NOT_RUNNING		= 2

	def __init__(self, db_host = "", db_port = None, db_timeout = 10, db_reconn_count = 2):
		"""
		:param str db_host: host name.			[ OBSOLETE ]
		:param int db_port: port.				[ OBSOLETE ]
		:param int db_timeout: disconnect timeout (seconds).
		:param int db_reconn_count: reconnect attempts count.

		Constructor.
		"""

		self.db_timeout = db_timeout
		self.db_reconn_count = db_reconn_count

		self.__verify_ssl = None
		self.__session = None
		self.__server_list = []

		# Init connection state
		self.disconnect()

	@property
	def db_timeout(self):
		return self.__db_timeout

	@db_timeout.setter
	def db_timeout(self, value):
		self.__db_timeout = int(value) if value and value > 0 else 10

	@property
	def db_reconn_count(self):
		return max(self.__db_reconn_count, len(self.__db_url_primary) - 1, len(self.__db_url_secondary) - 1)

	@db_reconn_count.setter
	def db_reconn_count(self, value):
		self.__db_reconn_count = int(value) if value and value >= 0 else 2

	@property
	def is_connected_by_client(self):
		return self.__is_connected_by_client

	@is_connected_by_client.setter
	def is_connected_by_client(self, value):
		self.__is_connected_by_client = bool(value)

	@property
	def db_user(self):
		return self.__db_user

	@db_user.setter
	def db_user(self, value):
		self.__db_user = value if value else ""

	@property
	def db_password(self):
		return self.__db_password

	@db_password.setter
	def db_password(self, value):
		self.__db_password = value if value else ""

	@property
	def db_long_token(self):
		return self.__db_long_token

	@db_long_token.setter
	def db_long_token(self, value):
		self.__db_long_token = str(value) if value else None

	@property
	def token(self):
		return self.__sid

	@token.setter
	def token(self, value):
		self.__sid = int(value) if value else -1

	@property
	def connected(self):
		return self.__sid != -1

	@property
	def query_id(self):
		return self.__query_id

	@query_id.setter
	def query_id(self, value):
		self.__query_id = int(value) if value else -1

	@property
	def query_id_next(self):
		self.__query_id += 1
		return self.__query_id

	@property
	def db_url_primary(self):
		return self.__db_url_primary[0] if self.__db_url_primary else ""

	@property
	def db_url_primary_ex(self):
		return ';'.join(self.__db_url_primary)

	@db_url_primary.setter
	def db_url_primary(self, value):
		self.__db_url_primary = [ format_rpc_url(host) for host in value.split(';') ] if value else []

	@property
	def db_url_secondary(self):
		return self.__db_url_secondary[0] if self.__db_url_secondary else self.db_url_primary

	@property
	def db_url_secondary_ex(self):
		return ';'.join(self.__db_url_secondary)

	@db_url_secondary.setter
	def db_url_secondary(self, value):
		self.__db_url_secondary = [ format_rpc_url(host) for host in value.split(';') ] if value else []

	@property
	def proxy(self):
		return self.__db_url_proxy

	@proxy.setter
	def proxy(self, value):
		self.__db_url_proxy = value.replace("http://", "").replace("https://", "") if value else ""

	@property
	def proxy_list(self):
		if len(self.__db_url_proxy) > 0:
			if len(self.__db_url_proxy.split("://")) < 2:
				return {"http": "http://" + self.__db_url_proxy, "https": "https://" + self.__db_url_proxy}
			else:
				return {"http": self.__db_url_proxy, "https": self.__db_url_proxy}
		return {}

	@property
	def session(self):
		if self.__session is None:
			self.__session = requests.Session()

		self.__session.headers.update({
			"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
			"Accept": "application/json-rpc",
			"Cache-Control": "no-store"
		})
		self.__session.cookies.update({
			"token": str(self.token)
		})
		self.__session.proxies.update(self.proxy_list)
		self.__session.timeout = self.db_timeout

		return self.__session

	@session.setter
	def session(self, value):
		if self.__session is not None:
			self.__session.close()
		self.__session = None

	@property
	def server_list(self):
		return self.__server_list
	
	@server_list.setter
	def server_list(self, value):
		self.__server_list = value if value and isinstance(value, list) else []

	@property
	def verify_ssl(self):
		return self.db_url_primary in URL_VERIFY if self.__verify_ssl is None else self.__verify_ssl

	@verify_ssl.setter
	def verify_ssl(self, value):
		self.__verify_ssl = None if value is None else bool(value)

	def is_connected(self):
		"""
		Returns True if current connection is active.
		"""

		return self.connected

	def disconnect(self):
		"""
		Force drop current connection settings.
		"""

		self.token = -1
		self.is_connected_by_client = False
		self.db_user = None
		self.db_password = None
		self.db_long_token = None
		self.query_id = 1
		self.db_url_primary = ""
		self.db_url_secondary = ""
		self.proxy = ""
		self.session = None
		self.__server_list = []

	def set_proxy(self, proxy_host):
		"""
		Set proxy address for all queries
		"""
		self.proxy = proxy_host

	def set_server(self, server_index):
		"""
		Set active server by index from server_list
		"""

		if isinstance(server_index, int) and server_index >= 0 and server_index < len(self.__server_list):
			srv_data = self.__server_list[server_index]

			self.token = srv_data["token"]
			self.db_long_token = srv_data["longToken"]
			self.db_url_primary = ';'.join([ srv_data["jrpc_main"] ] + srv_data["jrpc_alt"])
			self.db_url_secondary = None
		else:
			raise Exception("Invalid server index provided to database.set_server(): {}".format(server_index))

	def connect(self, db_user, db_password, primary_url="", secondary_url="", proxy=""):
		"""
		Connection and authentification by user/password pair.
		"""
		self.z_connect(None, db_user, db_password, primary_url, secondary_url, proxy)

	def connect_from_long_token(self, db_token, primary_url, secondary_url="", proxy=""):
		"""
		Connection and authentification by long token.
		"""
		self.z_connect(db_token, None, None, primary_url, secondary_url, proxy)

	def z_connect(self, db_token, db_user, db_password, primary_url, secondary_url, proxy):
		"""
		Connection and authentification internal.
		"""

		self.disconnect()

		direct_connection = primary_url is not None and len(primary_url) > 0
		primary_auth = bool(db_user and db_password)

		if not direct_connection:
			# Use lapi when connecting via login servers during primary auth
			primary_url = ';'.join( "{}/{}/rpc.php".format(url, "lapi" if primary_auth else "dapi") for url in DEFAULT_URL.split(';') )

		self.is_connected_by_client = False
		self.db_user = db_user
		self.db_password = db_password
		self.db_long_token = db_token
		self.proxy = proxy
		self.__server_list = []

		header = {
			"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
			"Accept": "application/json-rpc",
			"Cache-Control": "no-store"
		}
		payload = {
			"jsonrpc": "2.0",
			"id": self.query_id
		}

		if not(self.db_long_token or primary_auth):
			raise Exception("Connection credentials are empty")

		if primary_auth:
			payload["method"] = "sessionDirectStart" if direct_connection else "sessionStart"
			payload["params"] = [self.db_user, self.db_password, 655360]
		else:
			payload["method"] = "sessionStartLToken"
			payload["params"] = [self.db_long_token, 655360]

		res = None
		error_msg = ""

		url_list = primary_url.split(';')
		if not direct_connection: shuffle(url_list)

		for url in url_list:
			if len(url) == 0:
				continue

			self.db_url_primary = url

			for i in range(self.db_reconn_count + 1):
				try:
					response = requests.post(self.db_url_primary
						, headers = header
						, data = json.dumps(payload, default=json_serial)
						, timeout = 2 * self.db_timeout
						, proxies = self.proxy_list
						, verify = self.verify_ssl
					)
					payload["id"] = self.query_id_next

					if response.status_code == 200:
						res = json.loads(response.text)
						err = res.get("error", None)
						# Internal server error occured
						if err is not None:
							error_msg = text_unicode("{0} : {1}").format(text_unicode(err["code"]), text_unicode(err["message"]))
							res = None
							continue
						# Received normal response from server
						if len(res.get("result", [])) == 0:
							raise Exception("Login/password is invalid")
						break
					else:
						# Http request error occured
						error_msg = "{0} : {1}".format(response.status_code, response.reason)
				except requests.exceptions.RequestException as req_ex:
					if len(self.__db_url_primary) > 1:
						self.__db_url_primary = self.__db_url_primary[1:] + self.__db_url_primary[:1]
				except Exception as err:
					error_msg = error_unicode(err)
					res = None

			# Check result
			if res is not None:
				break

		if res is not None and len(res.get("result", [])) > 0:
			for conn_info in res["result"]:
				# Skip GCC auth data for now
				# not direct_connection and primary_auth
				if conn_info.get("id", None) == -1: continue				

				conn_name = "Direct connection" if direct_connection else conn_info["name"]
				conn_url = self.db_url_primary if direct_connection else conn_info["jrpc_main"] + u"/rpc.php" 
				
				conn_alt = self.__db_url_primary[1:] if direct_connection or conn_info.get("addr_rpc") is None else [ url + u"/rpc.php" for url in conn_info.get("addr_rpc", "").split('\n') ]

				user = conn_info.get("user", {})
				self.__server_list.append({
					"name": conn_name
					, "addr_jrpc": conn_url 
					, "jrpc_main": conn_url
					, "jrpc_alt": conn_alt
					, "uni_names": sorted(user.get("uni_names", []))
					, "token": int(conn_info.get("token", -1))
					, "longToken": conn_info.get("longToken", None)
				})

			# Select first server by default
			self.set_server(0)
		else:
			raise Exception("Connection error: {0}".format(error_msg))

	def connect_from_cerebro_client(self):
		"""
		Previously authentificated Cerebro user connects to the database.
		Such a connection may be established if the user has logged in Cerebro on the same workstation.
		Unlike the usual :py:func:`Database.connect()<py_cerebro.Database.connect>` which may close the
		concurrent Cerebro client connection (if connected with the same credentials) this function will
		leave the Cerebro client connection alive.

		:returns:
			connection status:
			* 0 - connection established;
			* 1 - connection not established (Cerebro client application is running, but user is not logged in);
			* 2 - connection not established (Cerebro client application is not running).

		::

			# Establishing connection with the database
			if db.connect_from_cerebro_client() != db.CLIENT_CONNECTED: # Attempting to connect via Cerebro client application.
				# If failed, connecting with login and password
				db.connect(db_user, db_password)

		.. seealso:: :py:meth:`connect() <py_cerebro.database.Database.connect>`.
		"""

		status = self.CLIENT_NOT_RUNNING
		conn = None

		self.disconnect()

		try:
			conn = socket.create_connection(("127.0.0.1", CEREBRO_PORT), 5)

			msg = struct.pack("II", CEREBRO_PROTO_VERSION, CEREBRO_PACKET_TYPE)
			header = struct.pack("II", CEREBRO_PROTO_HEADER, len(msg))

			conn.send(header + msg)
			data = socket_recvall(conn)

			res = struct.unpack_from("IIQI{0}s".format(len(data) - 20), data, 0)

			if res[0] == CEREBRO_PROTO_HEADER:
				session_id = int(res[2])
				if session_id == 0:
					status = self.CLIENT_NOT_CONNECTED
				else:
					cerebro_url = string_unicode(res[4].strip(b"\x00"))
					server_idx = 0

					try:
						urls = json.loads(cerebro_url)

						if "servers" in urls:
							for i, conn_info in enumerate(urls["servers"]):
								if (session_id, urls["primary"]) == (int(conn_info["token"]), conn_info["primary"]):
									server_idx = i

								self.__server_list.append({
									"name": conn_info["name"]
									, "addr_jrpc": conn_info["primary"] # obsolete
									, "jrpc_main": conn_info["primary"]
									, "jrpc_alt": conn_info.get("alternatives", [])
									, "uni_names": conn_info.get("uni_names", [])
									, "token": int(conn_info["token"])
									, "longToken": None
								})
						else:
							self.__server_list.append({
								"name": "Client"
								, "addr_jrpc": urls["primary"]
								, "jrpc_main": urls["primary"]
								, "jrpc_alt": []
								, "uni_names": []
								, "token": session_id
								, "longToken": None
							})

						self.proxy = urls.get("proxy", "")
						status = self.CLIENT_CONNECTED
					except ValueError:
						# Python 2 raises ValueError, Python 3 raises json.JSONDecodeError
						# a subclass of ValueError
						pg_url, pg_db = cerebro_url.split('@')[1].split('/')[:2]
						pg_url, pg_port = pg_url.split(':')
						pg_key = "{}:{}".format(pg_url, pg_db)

						if pg_key in URL_MAP:
							self.__server_list.append({
								"name": "Client"
								, "addr_jrpc": URL_MAP[pg_key] 
								, "jrpc_main": URL_MAP[pg_key]
								, "jrpc_alt": []
								, "uni_names": []
								, "token": session_id
								, "longToken": None
							})

							status = self.CLIENT_CONNECTED

					if status == self.CLIENT_CONNECTED:
						self.is_connected_by_client = True
						self.set_server(server_idx)

		except Exception as err:
			pass
		finally:
			if conn is not None: conn.close()

		return status

	def reconnect(self):
		"""
		Try to update connection session with current authentication params.
		"""

		# save current servers
		save_servers = self.__server_list
		current_server = 0
		if len(self.__server_list) > 1:
			for server in self.__server_list:
				if server["jrpc_main"] == self.db_url_primary or self.db_url_primary in server["jrpc_alt"]:					
					break
				current_server += 1

		if self.is_connected_by_client:
			self.connect_from_cerebro_client()
		elif self.db_long_token:
			self.connect_from_long_token(self.db_long_token, self.db_url_primary_ex, self.db_url_secondary, self.proxy)
		elif self.db_user and self.db_password:
			self.connect(self.db_user, self.db_password, self.db_url_primary_ex, self.db_url_secondary, self.proxy)

		# restore current servers
		if len(save_servers) > 1:
			while len(self.__server_list):
				server = self.__server_list.pop(0)				
				for index, save_server in enumerate(save_servers):
					if save_server["jrpc_main"] == server["jrpc_main"] or save_server["jrpc_main"] in server["jrpc_alt"]:
						save_servers[index]["jrpc_main"] = server["jrpc_main"]
						save_servers[index]["addr_jrpc"] = server["addr_jrpc"]
						save_servers[index]["token"] = server["token"]
						if server.get("longToken", None):
							save_servers[index]["longToken"] = server["longToken"]
						if len(server.get("jrpc_alt", [])) > 0:
							save_servers[index]["jrpc_alt"] = server["jrpc_alt"]
						if len(server.get("uni_names", [])) > 0:
							save_servers[index]["uni_names"] = server["uni_names"]				
						server = None
						break
				
				if server: 
					save_servers.append(server)

			self.__server_list = save_servers
			if len(self.__server_list) > current_server and self.__server_list[current_server].get("token", -1) > 0:
				self.set_server(current_server)

		return self.is_connected()

	def execute(self, query, *parameters):
		"""
		:param str query: query text.
		:param parameters: query parameters list.

		Executes the query and returns the result. The result has a form of a table (list pf tuples).
		"""

		return self.z_execute(False, query, *parameters)

	def z_execute(self, read_only, query, *parameters):
		"""
		:param bool read_only: check if query only reads data from DB.
		:param str query: query text.
		:param parameters: query parameters list.

		Executes the query and returns the result. The result has a form of a table (list pf tuples).
		"""

		ret = []

		if not self.is_connected() and not self.reconnect():
			raise Exception("Connection error: Reconnect attempt failed")

		args = [query.replace('%s', '?')]
		args.extend(parameters)

		payload = {
			"method": "queryMulti",
			"jsonrpc": "2.0",
			"params": args,
			"id": self.query_id
		}

		res = None
		error_msg = ""
		for i in range(self.db_reconn_count + 1):
			res = None
			payload["id"] = self.query_id_next
			db_url = self.db_url_secondary if read_only else self.db_url_primary

			try:
				response = self.session.post(db_url
					, data = json.dumps(payload, default=json_serial)
					, verify = self.verify_ssl
				)
				response.raise_for_status()
				res = json.loads(response.text)
			except requests.exceptions.RequestException as req_ex:
				# Http request error occured
				if hasattr(req_ex, 'response') and req_ex.response is not None:
					error_msg = "Connection error {0} : {1}".format(req_ex.response.status_code, req_ex.response.reason)
				else:
					error_msg = str(req_ex)
					# Cycle through reserve URLs
					if len(self.__db_url_primary) > 1 and self.__db_url_primary[0] == db_url:
						self.__db_url_primary = self.__db_url_primary[1:] + self.__db_url_primary[:1]
					if len(self.__db_url_secondary) > 1 and self.__db_url_secondary[0] == db_url:
						self.__db_url_secondary = self.__db_url_secondary[1:] + self.__db_url_secondary[:1]
				continue
			except Exception as e:
				# Python exception. Interrupt execution
				error_msg = str(e)
				break

			if res is not None:
				err = res.get("error", None)
				# Success
				if err is None: break
				# Internal server error occured
				res = None
				error_msg = text_unicode("{0} : {1}").format(text_unicode(err["code"]), text_unicode(err["message"]))
				# Reconnect if requested by server
				if err["code"] in RECONNECT_ERROR_CODES: continue
				elif ("#0" in err["message"] or "#1" in err["message"]) and re.search(r"#[01](?:\D|$)", err["message"]):
					# Require connection using credentials
					if self.is_connected_by_client:
						self.disconnect()
						raise Exception("Client connection is invalid. Reconnect using credentials. #0")
					self.reconnect()
					continue
				# Interrupt execution
				break

		# Load the result if not empty
		if res is not None:
			query_count = len(res.get("result", []))
			for iquery in range(query_count):
				query_ret = []
				for irow, row in enumerate(res["result"][iquery]["rows"]):
					query_ret.append([])
					for icol, col in enumerate(row):
						type = res["result"][iquery]["columns"][icol]["type"]
						if col is None:
							query_ret[irow].append(col)
						elif type in TYPES_INT:
							query_ret[irow].append(int(col))
						elif type in TYPES_FLOAT:
							query_ret[irow].append(float(col))
						elif type in TYPES_DATETIME:
							query_ret[irow].append(iso8601.parse_date(col))
						else:
							query_ret[irow].append(col)
				if query_count == 1:
					ret = query_ret
				else:
					ret.append(query_ret)
		else:
			raise Exception(error_msg)

		return ret

	def current_user_id(self):
		"""
		:returns: the logged in user's ID.
		"""

		user_id = self.z_execute(True, 'select get_usid()')
		if len(user_id) == 1:
			return user_id[0][0]

		return None
	
	def current_user(self):
		"""
		:returns: current user data.

		The table fields are described in the module dbtypes: :py:const:`USER_DATA_...<py_cerebro.dbtypes.USER_DATA_>`		
		"""
		user = self.z_execute(True, 'select uid, "userNameDisplay"(uid) as name, "userGetFlags"(uid) as flags' \
									', "userGetLogin"(uid) as lid, "userGetFirstName"(uid) as firstname, "userGetLastName"(uid) as lastname' \
									', "userGetEmail"(uid) as email, "userGetPhone"(uid) as phone, "userGetIcq"(uid) as icq from "get_usid"() as uid')
		return user[0]

	def root_tasks(self):
		"""
		:returns: table of root tasks.

		The table fields are described in the module dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`

		"""

		tasks = self.z_execute(True, 'select uid from "_task_list_00"(0,0)')

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return self.z_execute(True, 'select * from "taskQuery_11"(?::bigint[])', ids)

	def to_do_task_list(self, user_id, with_done_task):
		"""
		:returns: table of tasks the user is allocated to.

		:param user_id: ID of the user or array of user IDs (or of a material resource).
		:type user_id: int, set(int, ) or list(int, )
		:param bool with_done_task: if "True", then the returned table will include completed tasks (progress = 100%) also, otherwise - with incomplete tasks only.

		The table fields are described in the module dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
		"""

		tasks = self.z_execute(True, 'select uid from "taskAssigned_byUsers"(?, ?)', to_array(user_id), with_done_task)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return self.z_execute(True, 'select * from "taskQuery_11"(?::bigint[])', ids)

	def task(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: task data.

		The table fields are described in the module dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`

		.. seealso:: :py:meth:`tasks() <py_cerebro.database.Database.tasks>`.
		"""

		dset = set()
		dset.add(task_id)
		task = self.z_execute(True, 'select * from "taskQuery_11"(?::bigint[])', dset)
		if len(task) == 1:
			return task[0]

		return None

	def tasks(self, task_ids):
		"""
		:param array task_ids: array of task IDs.
		:returns: tasks data.

		The table fields are described in the module dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`

		.. seealso:: :py:meth:`task() <py_cerebro.database.Database.task>`.
		"""

		tasks = self.z_execute(True, 'select * from "taskQuery_11"(?::bigint[])', task_ids)
		if len(tasks) > 0:
			return tasks

		return None

	def copy_tasks(self, task_id, tasks_list, \
				flags = COPY_TASKS_SUB_TASKS|COPY_TASKS_INTERNAL_LINKS|COPY_TASKS_TAGS|COPY_TASKS_ASSIGNED_USERS|COPY_TASKS_EVENTS):
		"""
		:param task_id: copied task ID.
		:type task_id: int, set(int, ) or list(int, )
		:param set tasks_list: list of tuples [(id1, name1), (id2, name2),].
		:param int flags: flags.
		:returns: IDs of new tasks.

		Copy tasks.

		Full description of flags in dbtypes: :py:const:`COPY_TASKS_...<py_cerebro.dbtypes.COPY_TASKS_>`

		If you need replicate task, you must set tasks_list as list of tuples with one task_id and different names. For example:

		[(123, 'test_task02'), (123, 'test_task03'), (123, 'test_task04'), (123, 'test_task05')]

		123 - copied task ID.
		'test_task02', 'test_task03', ... - names of new tasks.

		::

			# Копируем в задачу 0 задачи 1(2 копии), 2 и 3
			to_do_task_list = db.to_do_task_list(db.current_user_id(), True)
			lst_copy = [(to_do_task_list[1][dbtypes.TASK_DATA_ID], 'Копия задачи 1(1)'),
				(to_do_task_list[1][dbtypes.TASK_DATA_ID], 'Копия задачи 1(2)'),
				(to_do_task_list[2][dbtypes.TASK_DATA_ID], 'Копия задачи 2'),
				(to_do_task_list[3][dbtypes.TASK_DATA_ID], 'Копия задачи 3')]
			db.copy_tasks(to_do_task_list[0][dbtypes.TASK_DATA_ID], lst_copy)
			# В задачу 0 добавлено 4 новых задачи
		"""

		tids = []
		names = []

		for task_list_id, task_list_name in tasks_list:
			t_name = str(task_list_name)
			if re.search(RE_TASK_NAME, t_name): raise Exception("Name is incorrect. Symbols \\ / # : ? & ' \" , + | are not allowed")
			tids.append(task_list_id)
			names.append(str(t_name.strip()))
		tasks = self.z_execute(False, 'select "dupVTask"(?::bigint[],?::text[],?::bigint,?::int,array[]::int[])', tids, names, task_id, flags)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_children(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: subtasks table.

		The table fields are described in the module dbtypes: :py:const:`TASK_DATA_...<py_cerebro.dbtypes.TASK_DATA_>`
		"""

		tasks = self.z_execute(True, 'select uid from "_task_list_00"(?::bigint,0)', task_id)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return self.z_execute(True, 'select * from "taskQuery_11"(?::bigint[])', ids)


	def task_allocated(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: the table of users allocated to the task.

		The table fields are described in the module dbtypes: :py:const:`TASK_ALLOCATED_...<py_cerebro.dbtypes.TASK_ALLOCATED_>`
		"""

		return self.z_execute(True, 'select uid, "userNameDisplay"(uid) as name, "userGetFlags"(uid) as flags' \
									' from "assignedUsersTask"(?::bigint) as uid order by name', task_id)

	def task_attachments(self, task_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:returns: table of files attached to the task(s).

		The table fields are described in the module dbtypes: :py:const:`ATTACHMENT_DATA_...<py_cerebro.dbtypes.ATTACHMENT_DATA_>`

		One attachment can take 1 to 5 entries in the table.
		The attachment records are grouped by the group ID - :py:const:`ATTACHMENT_DATA_GROUP_ID<py_cerebro.dbtypes.ATTACHMENT_DATA_GROUP_ID>`.
		The records of one attachment are tagged with :py:const:`ATTACHMENT_DATA_TAG<py_cerebro.dbtypes.ATTACHMENT_DATA_TAG>`, and stand for a particular parameter of the attachment.

		Attachments fall into two types: a file and a link to a file. In case of file the attachment has
		a tag :py:const:`ATTACHMENT_TAG_FILE<py_cerebro.dbtypes.ATTACHMENT_TAG_FILE>`, which contains the hash of the file on a Cargador-operated file storage.
		In case of a link, the attachment is tagged with :py:const:`ATTACHMENT_TAG_LINK<py_cerebro.dbtypes.ATTACHMENT_TAG_LINK>`.
		This entry has no hash, it contains a full path to file :py:const:`ATTACHMENT_DATA_FILE_NAME<py_cerebro.dbtypes.ATTACHMENT_DATA_FILE_NAME>` in its name field.
		The entry tagged with :py:const:`ATTACHMENT_TAG_REVIEW<py_cerebro.dbtypes.ATTACHMENT_TAG_REVIEW>` is available only if the file has a Mirada review over it.
		The entry with a thumbnail tag :py:const:`ATTACHMENT_TAG_THUMB...<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, is available only if the file is a picture or video.
		If the file is a picture, it has only one entry :py:const:`ATTACHMENT_TAG_THUMB1<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, if it is a video -- three entries.
		"""

		return self.z_execute(True, 'select * from "listAttachmentsTasks"(?::bigint[], false)', to_array(task_id))

	def task_links(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: the table of task connections with other tasks.

		The table fields are described in the module dbtypes: :py:const:`TASK_LINK_...<py_cerebro.dbtypes.TASK_LINK_>`
		"""

		return self.z_execute(True, 'select * from "ggLinks_byTask"(?::bigint)', task_id)

	def task_definition(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: data of *Definition* message type.

		The table fields are described in the module dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
		"""

		id = self.z_execute(True, 'select "getTaskDefinitionId"(?::bigint)', task_id)
		if (len(id) == 1 and id[0][0] != None):
			dset = set()
			dset.add(id[0][0])
			definition = self.z_execute(True, 'select * from "eventQuery_08"(?::bigint[])', dset)
			if len(definition) == 1:
				return definition[0]

		return None

	def task_messages(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: the table of messages in the task.

		The table fields are described in the module dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
		The message types are described in the module dbtypes: :py:const:`MESSAGE_TYPE_...<py_cerebro.dbtypes.MESSAGE_TYPE_>`
		"""

		messs = self.z_execute(True, 'select uid from "_event_list"(?::bigint, false)', task_id)
		ids = set()
		for mess in messs:
			ids.add(mess[0])

		return self.z_execute(True, 'select * from "eventQuery_08"(?::bigint[])', ids)

	def task_possible_statuses(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: the table of statuses, which can be set for the task.

		The table fields are described in the module dbtypes: :py:const:`STATUS_DATA_...<py_cerebro.dbtypes.STATUS_DATA_>`

		In the Cerebro system for each status, permissions for switching each status are set.
		In addition, each status has a flag of inheritance.
		On the task-containers you can set only the statuses that have this flag enabled.
		Therefore, the list of possible statuses depends on user rights, the current status,
		as well as the presence / lack of sub-tasks in the task.
		"""

		return self.z_execute(True, 'select * from "statusListByTask_01"(?::bigint)', task_id)

	def task_checks(self, task_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:returns: table of checks assigned to the task(s) checklist(s).

		The table fields are described in the module dbtypes: :py:const:`TASK_CHECK_DATA_...<py_cerebro.dbtypes.TASK_CHECK_DATA_>`
		"""

		return self.z_execute(True, 'select * from "taskChecksColumn"(?::bigint)', to_array(task_id))

	def task_user_note(self, task_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:returns: task user note.
		"""

		return self.z_execute(True, 'select * from "taskUserNote"(?::bigint)', to_array(task_id))

	def message(self, message_id):
		"""
		:param int message_id: message ID.
		:returns: message data.

		The table fields are described in the module dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
		The message types are described in the module dbtypes: :py:const:`MESSAGE_TYPE_...<py_cerebro.dbtypes.MESSAGE_TYPE_>`
		"""

		dset = set()
		dset.add(message_id)
		mess = self.z_execute(True, 'select * from "eventQuery_08"(?::bigint[])', dset)
		if len(mess) == 1:
			return mess[0]

		return None

	def messages(self, message_ids):
		"""
		:param array message_ids: array of message IDs.
		:returns: messages data.

		The table fields are described in the module dbtypes: :py:const:`MESSAGE_DATA_...<py_cerebro.dbtypes.MESSAGE_DATA_>`
		The message types are described in the module dbtypes: :py:const:`MESSAGE_TYPE_...<py_cerebro.dbtypes.MESSAGE_TYPE_>`
		"""

		mess = self.z_execute(True, 'select * from "eventQuery_08"(?::bigint[])', message_ids)
		if len(mess) > 0:
			return mess

		return None

	def message_attachments(self, message_id):
		"""
		:param message_id: message ID or array of message IDs.
		:type message_id: int, set(int, ) or list(int, )
		:returns: table of files attached to the message(s).

		The table fields are described in the module dbtypes: :py:const:`ATTACHMENT_DATA_...<py_cerebro.dbtypes.ATTACHMENT_DATA_>`

		One attachment can take 1 to 5 entries in the table.
		The attachment records are grouped by the group ID - :py:const:`ATTACHMENT_DATA_GROUP_ID<py_cerebro.dbtypes.ATTACHMENT_DATA_GROUP_ID>`.
		The records of one attachment are tagged with :py:const:`ATTACHMENT_DATA_TAG<py_cerebro.dbtypes.ATTACHMENT_DATA_TAG>`, and stand for a particular parameter of the attachment.

		Attachments fall into two types: a file and a link to a file. In case of file the attachment has
		a tag :py:const:`ATTACHMENT_TAG_FILE<py_cerebro.dbtypes.ATTACHMENT_TAG_FILE>`, which contains the hash of the file on a Cargador-operated file storage.
		In case of a link, the attachment is tagged with :py:const:`ATTACHMENT_TAG_LINK<py_cerebro.dbtypes.ATTACHMENT_TAG_LINK>`.
		This entry has no hash, it contains a full path to file :py:const:`ATTACHMENT_DATA_FILE_NAME<py_cerebro.dbtypes.ATTACHMENT_DATA_FILE_NAME>` in its name field.
		The entry tagged with :py:const:`ATTACHMENT_TAG_REVIEW<py_cerebro.dbtypes.ATTACHMENT_TAG_REVIEW>` is available only if the file has a Mirada review over it.
		The entry with a thumbnail tag :py:const:`ATTACHMENT_TAG_THUMB...<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, is available only if the file is a picture or video.
		If the file is a picture, it has only one entry :py:const:`ATTACHMENT_TAG_THUMB1<py_cerebro.dbtypes.ATTACHMENT_TAG_THUMB1>`, if it is a video -- three entries.
		"""

		return self.z_execute(True, 'select * from "listAttachmentsArray"(?::bigint[], false)', to_array(message_id))

	def users(self):
		"""
		:returns: table of users/material resources.

		The table fields are described in the module dbtypes: :py:const:`USER_DATA_...<py_cerebro.dbtypes.USER_DATA_>`

		A material resource has a flag set :py:const:`USER_FLAG_IS_RESOURCE<py_cerebro.dbtypes.USER_FLAG_IS_RESOURCE>`.
		You can check the state of the flag with the function :py:func:`cclib.has_flag<py_cerebro.cclib.has_flag>`::

			if cclib.has_flag(user[dbtypes.USER_DATA_FLAGS], dbtypes.USER_FLAG_IS_RESOURCE): #if it is material resource
				# actions
		"""

		return self.z_execute(True, 'select uid, "userNameDisplay"(uid) as name, "userGetFlags"(uid) as flags' \
									', "userGetLogin"(uid) as lid, "userGetFirstName"(uid) as firstname, "userGetLastName"(uid) as lastname' \
									', "userGetEmail"(uid) as email, "userGetPhone"(uid) as phone, "userGetIcq"(uid) as icq from "userList"() order by name')

	def activities(self):
		"""
		:returns: table of activities.

		The table fields are described in the module dbtypes: :py:const:`ACTIVITY_DATA_...<py_cerebro.dbtypes.ACTIVITY_DATA_>`
		"""

		return self.z_execute(True, 'select uid, name, color, unid from "listActivities"(false)')

	def statuses(self):
		"""
		:returns: the table of all statuses.

		The table fields are described in the module dbtypes: :py:const:`STATUS_DATA_...<py_cerebro.dbtypes.STATUS_DATA_>`
		"""
		return self.z_execute(True, 'select * from "statusList_01"()')

	def checks(self):
		"""
		:returns: the table of all checks.

		The table fields are described in the module dbtypes: :py:const:`CHECK_DATA_...<py_cerebro.dbtypes.CHECK_DATA_>`
		"""
		return self.z_execute(True, 'select * from "checkList"()')

	def add_task(self, parent_id, name, activity_id = 0):
		"""
		:param int parent_id: parent task ID.
		:param str name: name of the new task.
		:param int activity_id: activity type ID. Default - '0' (No activity).

		:returns: new task ID.

		New task creation. The symbols: **\\\\ / # : ? & ' " , + |** cannot be used in a task name.

		.. note:: To send a notice to the user about the new task, you must create
			:py:meth:`the "Defenition" type message <py_cerebro.database.Database.add_definition>` in the task.
		"""

		if re.search(RE_TASK_NAME, name): raise Exception("Name is incorrect. Symbols \\ / : ? ' \" | * < > ] are not allowed")

		return self.z_execute(False, 'select "newTask_00"(?::bigint,?,?::bigint,true)', parent_id, name.strip(), activity_id)[0][0]

	def task_set_name(self, task_id, name):
		"""
		:param int task_id: task ID.
		:param str name: new task name.

		Sets a new name for a task.
		It cannot contain the following symbols: **\\\\ / # : ? & ' " , + |**.
		"""

		if re.search(RE_TASK_NAME, name): raise Exception("Name is incorrect. Symbols \\ / # : ? & ' \" , + | are not allowed")

		return self.z_execute(False, 'select "taskSetName"(?::bigint,?)', task_id, name.strip())[0][0]

	def task_set_activity(self, task_id, activity_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param int activity_id: activity ID.

		Sets an activity type for a task. Activity ID = 0 sets the task activity type to 'No activity'.
		"""

		tasks = self.z_execute(False, 'select "taskSetActivity_a"(?::bigint[],?::bigint)', to_array(task_id), activity_id)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_status(self, task_id, status_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param int status_id: status ID.

		Sets a status for a task. Status ID = None sets the task status to 'No Status'.
		"""

		tasks = self.z_execute(False, 'select "taskSetStatus_a"(?::bigint[],?::bigint)', to_array(task_id), status_id)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_priority(self, task_id, prior):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param int prior: Task priority value.

		Sets task priority level.

		Priority values are described in the module dbtypes: :py:const:`TASK_PRIORITY_...<py_cerebro.dbtypes.TASK_PRIORITY_>`
		"""

		tasks = self.z_execute(False, 'select "taskSetPriority_a"(?::bigint[],?::smallint)', to_array(task_id), prior)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_flag(self, task_id, flag, is_set):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param int flag: flag value.
		:param bool is_set: current flag setting.

		Sets a flag for a task.
		If *is_set* is True, the flag sets, otherwise - resets.

		::

			# Mark a task as archived
			db.task_set_flag(task_id, dbtypes.TASK_FLAG_ARCHIVED, True)

		Flag values are described in the module dbtypes: :py:const:`TASK_FLAG_...<py_cerebro.dbtypes.TASK_FLAG_>`
		"""

		newFlag = 0
		if is_set:
			newFlag = 1 << flag

		mask = 1 << flag

		self.z_execute(False, 'select "taskSetFlagsMulti"(?::bigint[],?,?)', to_array(task_id), newFlag, mask)

	def task_set_progress(self, task_id, progress):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param int progress: progress value.

		Sets progress value for a task.
		If value is 100, the task is considered complete (Done).
		If value is set to None, the task progress value is reset to a sum of progress values of its subtasks.
		"""

		tasks = self.z_execute(False, 'select "taskSetProgress_a"(?::bigint[],?::smallint)', to_array(task_id), progress)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_planned_time(self, task_id, hours):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param float hours: planned working time (hours).

		Sets the time planned to fulfill the task in hours.
		If hours argument is set to None, the planned time is reset.
		After that the planned time is calculated according to calendar time frames and current work schedule of allocated user(s).
		"""

		tasks = self.z_execute(False, 'select "taskSetPlanned_a"(?::bigint[],?)', to_array(task_id), hours)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_start(self, task_id, time):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param float time: days from 01.01.2000.

		Sets the starting moment for a task, in days from 01.01.2000 (UTC time)

		If time = None, the start time value is reset.
		After resetting the starting point is calculated according to the task connections and the schedule.

		::

			db.task_set_start({task_id, task_id1}, 4506.375) # starting point is May 03, 2012 9:00am UTC

		An example of setting the starting time equal to current time

		::

			import datetime

			datetime_now = datetime.datetime.utcnow()
			datetime_2000 = datetime.datetime(2000, 1, 1)
			timedelta = datetime_now - datetime_2000
			days = timedelta.total_seconds()/(24*60*60)

			db.task_set_start({task_id, task_id1}, days)
		"""

		tasks = self.z_execute(False, 'select "ggSetTaskOffset_a"(?::bigint[],?::double precision)', to_array(task_id), time)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_finish(self, task_id, time):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param float time: days from 01.01.2000 (UTC),

		Sets the task finish time, in days from 01.01.2000 (UTC time).

		If time = None, the finish time value is reset.
		After resetting the finish time is calculated according to the schedule and the planned working time.

		::

			db.task_set_finish(task_id, 4506.75) # the finish time is 03.05.2012 18:00 UTC

		An example of setting the task finish time in 3 days ahead from current time

		::

			import datetime

			datetime_now = datetime.datetime.utcnow()
			datetime_2000 = datetime.datetime(2000, 1, 1)
			timedelta = datetime_now - datetime_2000
			days = timedelta.total_seconds()/(24*60*60) + 3

			db.task_set_finish(task_id, days)
		"""

		tasks = self.z_execute(False, 'select "ggSetTaskStop_a"(?::bigint[],?::double precision)', to_array(task_id), time)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids


	def task_set_budget(self, task_id, budget):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param float budget: budgeted amount (in abstract currency units).

		Sets the task budget.

		If set to None, the current budget value is reset to the sum of its subtask budgets.
		"""

		tasks = self.z_execute(False, 'select "taskSetCosts_a"(?::bigint[],?)', to_array(task_id), budget)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_user_note(self, task_id, user_note):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param str user_note: task user note value.

		Sets user note for a task(s).
		"""

		tasks = self.z_execute(False, 'select "taskSetUserNote_a"(?::bigint[],?)', to_array(task_id), user_note)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_allocated(self, task_id, user_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param user_id: ID of user/material resource or array of ID of user/material resource.
		:type user_id: int, set(int, ) or list(int, )

		Allocates a user to a task.

		.. note:: To send a notice to the user about the assigned task, you must have
			:py:meth:`the "Defenition" type message <py_cerebro.database.Database.task_definition>` in the task.
		"""

		tasks = self.z_execute(False, 'select "userAssignmentTask_a"(?::bigint[],?,?)', to_array(task_id), to_array(user_id), 1)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_remove_allocated(self, task_id, user_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param user_id: ID of user/material resource or array of ID of user/material resource.
		:type user_id: int, set(int, ) or list(int, )

		Dismisses an allocated user from a task.
		"""

		tasks = self.z_execute(False, 'select "userAssignmentTask_a"(?::bigint[],?,?)', to_array(task_id), to_array(user_id), 0)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_hashtags(self, task_id, hashtags):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param hashtags: array of hashtags(every hashtag should be written in one word without spaces).
		:type hashtags: str, set(str, ) or list(str, )

		Sets the task hashtags.
		"""

		tasks = self.z_execute(False, 'select "htSetTask"(?::bigint[],?::text[],?)', to_array(task_id), hashtags, True)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_hashtags(self, task_id):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:returns: array of hashtag values.
		:rtype: list(str, )

		Gets the task hashtags.

		::
			# Работа с хэштегами задачи
			to_do_task_list = db.to_do_task_list(db.current_user_id(), True)
			db.task_set_hashtags(to_do_task_list[0][dbtypes.TASK_DATA_ID], {'хэштег1', 'хэштег2', 'хэштег3'}) # присваиваем задаче массив хэштегов
			db.task_remove_hashtags(to_do_task_list[0][dbtypes.TASK_DATA_ID], 'хэштег2') # удаляем хэштег
			hashtags = db.task_hashtags(to_do_task_list[0][dbtypes.TASK_DATA_ID]) # получаем хэштеги задачи
			print('Хэштеги задачи ', hashtags) # распечатываем хэштеги

		"""

		ht_list = self.z_execute(True, 'select * from "htTask"(?::bigint)', task_id)
		hashtags = [ ht[3] for ht in ht_list ]

		return hashtags

	def task_remove_hashtags(self, task_id, hashtags):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param hashtags: array of hashtags(every hashtag should be written in one word without spaces).
		:type hashtags: str, set(str, ) or list(str, )

		Removes the task hashtags.
		"""

		tasks = self.z_execute(False, 'select "htSetTask"(?::bigint[],?::text[],?)', to_array(task_id), to_array(hashtags), False)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_checks(self, task_id, checks):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param checks: array of checks.
		:type checks: str, set(str, ) or list(str, )
		:returns: new check items IDs.
		:rtype: list(int, )

		Sets the task checks.
		"""
		checks = to_array(checks)
		tchecks = []
		for check in checks:
			t_name = str(check)
			tchecks.append(str(t_name.strip()))

		res = self.z_execute(False, 'select "checkTaskSet"(?::bigint[],?::text[],?)', to_array(task_id), tchecks, True)

		ids = set()
		for check in res:
			ids.add(check[0])

		return ids

	def task_set_checks_value(self, task_id, check_id, value):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param check_id: ID of check item or array of ID of check items.
		:type check_id: int, set(int, ) or list(int, )
		:param int value: value checks item (0 or 1).

		Sets the task checks value.
		"""

		self.z_execute(False, 'select "checkTaskSetValue"(?::bigint[],?::bigint[],?)', to_array(task_id), to_array(check_id), value)

	def task_remove_checks(self, task_id, checks):
		"""
		:param task_id: task ID or array of task IDs.
		:type task_id: int, set(int, ) or list(int, )
		:param checks: array of checks.
		:type checks: str, set(str, ) or list(str, )

		Removes the task checks.
		"""

		checks = to_array(checks)
		tchecks = []
		for check in checks:
			t_name = str(check)
			tchecks.append(str(t_name))

		res = self.z_execute(False, 'select "checkTaskSet"(?::bigint[],?::text[],?)', to_array(task_id), tchecks, False)

		ids = set()
		for check in res:
			ids.add(check[0])

		return ids

	def message_set_hashtags(self, message_id, hashtags):
		"""
		:param message_id: message ID or array of message IDs.
		:type message_id: int, set(int, ) or list(int, )
		:param hashtags: array of hashtags(every hashtag should be written in one word without spaces).
		:type hashtags: str, set(str, ) or list(str, )

		Sets the message hashtags.
		"""

		tasks = self.z_execute(False, 'select "htSetEvent"(?::bigint[],?,?)', to_array(message_id), to_array(hashtags), True)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def message_hashtags(self, message_id):
		"""
		:param message_id: message ID or array of message IDs.
		:type message_id: int, set(int, ) or list(int, )
		:returns: array of hashtag values.
		:rtype: list(str, )

		Gets the message hashtags.
		"""

		hashtags = []

		ht_str = self.z_execute(True, 'select * from "htAggregateEvent"(?::bigint)', message_id)[0][0]
		if ht_str:
			ht_schema = self.z_execute(True, 'select * from "htSchemaList"()')
			ht_list = { int(ht_val[2:], 16) for ht_val in ht_str.split() }
			hashtags = [ ht[3] for ht in ht_schema if ht[1] in ht_list ]

		return hashtags

	def message_remove_hashtags(self, message_id, hashtags):
		"""
		:param message_id: message ID or array of message IDs.
		:type message_id: int, set(int, ) or list(int, )
		:param hashtags: array of hashtags(every hashtag should be written in one word without spaces).
		:type hashtags: str, set(str, ) or list(str, )

		Removes the message hashtags.
		"""

		tasks = self.z_execute(False, 'select "htSetEvent"(?::bigint[],?,?)', to_array(message_id), to_array(hashtags), False)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def attachment_set_hashtags(self, attachment_id, hashtags):
		"""
		:param attachment_id: attachment ID or array of attachment IDs.
		:type attachment_id: int, set(int, ) or list(int, )
		:param hashtags: array of hashtags(every hashtag should be written in one word without spaces).
		:type hashtags: str, set(str, ) or list(str, )

		Sets the attachment hashtags.

		.. note:: Recommended for attachments with tag ATTACHMENT_DATA_TAG value: ATTACHMENT_TAG_FILE or ATTACHMENT_TAG_LINK.
		"""

		tasks = self.z_execute(False, 'select "htSetAttachment"(?::bigint[],?,?)', to_array(attachment_id), to_array(hashtags), True)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def attachment_hashtags(self, attachment_id):
		"""
		:param attachment_id: attachment ID or array of attachment IDs.
		:type attachment_id: int, set(int, ) or list(int, )
		:returns: array of hashtag values.
		:rtype: list(str, )

		Gets the attachment hashtags.

		.. note:: Recommended for attachments with tag ATTACHMENT_DATA_TAG value: ATTACHMENT_TAG_FILE or ATTACHMENT_TAG_LINK.
		"""

		hashtags = []

		ht_str = self.z_execute(True, 'select * from "htAggregateAttachment"(?::bigint)', attachment_id)[0][0]
		if ht_str:
			ht_schema = self.z_execute(True, 'select * from "htSchemaList"()')
			ht_list = { int(ht_val[2:], 16) for ht_val in ht_str.split() }
			hashtags = [ ht[3] for ht in ht_schema if ht[1] in ht_list ]

		return hashtags

	def attachment_remove_hashtags(self, attachment_id, hashtags):
		"""
		:param attachment_id: attachment ID or array of attachment IDs.
		:type attachment_id: int, set(int, ) or list(int, )
		:param hashtags: array of hashtags(every hashtag should be written in one word without spaces).
		:type hashtags: str, set(str, ) or list(str, )

		Removes the attachment hashtags.

		.. note:: Recommended for attachments with tag ATTACHMENT_DATA_TAG value: ATTACHMENT_TAG_FILE or ATTACHMENT_TAG_LINK.
		"""

		tasks = self.z_execute(False, 'select "htSetAttachment"(?::bigint[],?,?)', to_array(attachment_id), to_array(hashtags), False)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def set_link_tasks(self, first_task_id, second_task_id):
		"""
		:param int first_task_id: predecessor task ID.
		:param int second_task_id: follower task ID.
		:returns: connection ID.
		:rtype: int

		Creates a sequence of tasks.
		"""

		return self.z_execute(False, 'select "ggLinkAdd_a"(?::bigint[],?::bigint[],?)', to_array(first_task_id), to_array(second_task_id), 0)[0][0]

	def drop_link_tasks(self, link_id):
		"""
		:param int link_id: connection ID.

		Breaks a sequential connection between tasks.
		"""

		self.z_execute(False, 'select "ggLinkDel_a"(?::bigint[],?)', to_array(link_id), True)

	def add_definition(self, task_id, html_text):
		"""
		:param int task_id: task ID.
		:param str html_text: message text in html.
		:returns: new message ID.
		:rtype: int

		Adds a message of "Definition" type.
		"""

		return self.z_execute(False, 'select "eventNew"(?,?::bigint,?,?,?,?)', None, task_id, html_text, MESSAGE_TYPE_DEFINITION, None, None)[0][0]

	def add_review(self, task_id, message_id, html_text, minutes = None):
		"""
		:param int task_id: task ID.
		:param int message_id: ID of the message being replied to.
		:param str html_text: message text in html.
		:param int minutes: confirmed working time, minutes.
		:returns: new message ID.
		:rtype: int

		Adds a message of "Review" type.
		"""

		return self.z_execute(False, 'select "eventNew"(?,?::bigint,?,?,?,?)', None, task_id, html_text, MESSAGE_TYPE_REVIEW, message_id, minutes)[0][0]

	def add_client_review(self, task_id, message_id, html_text):
		"""
		:param int task_id: task ID.
		:param int message_id: ID of the message being replied to.
		:param str html_text: message text in html.
		:returns: new message ID.
		:rtype: int

		Adds a message of "Client Review" type.
		"""

		return self.z_execute(False, 'select "eventNew"(?,?::bigint,?,?,?,?)', None, task_id, html_text, MESSAGE_TYPE_CLIENT_REVIEW, message_id, None)[0][0]

	def add_report(self, task_id, message_id, html_text, minutes):
		"""
		:param int task_id: task ID.
		:param int message_id: ID of the message being replied to.
		:param str html_text: message text in html.
		:param int minutes: signed off working time, minutes.
		:returns: new message ID.
		:rtype: int

		It's very important to set minutes for report.
		If minutes 0 or None report will not be added to statistic.

		Adds a message of "Report" type.
		"""

		if minutes == None:
			minutes = 0

		return self.z_execute(False, 'select "eventNew"(?,?::bigint,?,?,?,?)', None, task_id, html_text, MESSAGE_TYPE_REPORT, message_id, minutes)[0][0]

	def add_resource_report(self, task_id, message_id, resource_id, html_text, minutes):
		"""
		:param int task_id: task ID.
		:param int message_id: ID of the message being replied to.
		:param int resource_id: идентификатор материального ресурса, за который пишется отчет.
		:param str html_text: message text in html.
		:param int minutes: signed off working time, minutes.
		:returns: new message ID.
		:rtype: int

		Adds a message of "Resource Report" type.
		"""

		return self.z_execute(False, 'select "eventNew"(?,?::bigint,?,?,?,?)', resource_id, task_id, html_text, MESSAGE_TYPE_RESOURCE_REPORT, message_id, minutes)[0][0]

	def add_note(self, task_id, message_id, html_text):
		"""
		:param int task_id: task ID.
		:param int message_id: ID of the message being replied to.
		:param str html_text: message text in html.
		:returns: new message ID.
		:rtype: int

		Adds a message of "Note" type.
		"""

		return self.z_execute(False, 'select "eventNew"(?,?::bigint,?,?,?,?)', None, task_id, html_text, MESSAGE_TYPE_NOTE, message_id, None)[0][0]

	def add_attachment(self, message_id, carga, filename, thumbnails, description, as_link, path = '', flags = 0):
		"""
		:param int message_id: message ID.
		:param py_cerebro.cargador.Cargador carga: object of class :py:class:`cargador.Cargador<py_cerebro.cargador.Cargador>`, to import files to a file storage.
		:param str filename: full path to file.
		:param thumbnails: a list of paths to thumbnail files (3 max). Thumbnail size must be 512x512 pixels. Format - JPG or PNG.
		:param str description: comments to the attachment.
		:param bool as_link: method of file attachment to the message:
			True - file is attached as a link;
			False - file is imported physically to a file storage.

		Attaching a file to a message.

		**Using for thumbnail generation.**

		If a file is a picture or a video, it may have thumbnails to display in Cerebro forum.
		A video file may have up to 3 thumnails (the first frame, the middle frame or the last frame).
		By default thumbnails are generated by Mirada player, bundled with Cerebro.

		::

			#An example of generating thumbnails with Mirada.

			gen_path = os.path.dirname(filename) # Selecting the folder with the attached file as the folder to save thumbnails in
			mirada_path = './mirada' # path to Mirada executable

			# Executing Mirada with necessary keys
			res_code = subprocess.call([mirada_path, filename, '-temp', gen_path, '-hide'])
			#-temp - folder to generate thumbnails to
			#-hide - key to launch Mirada in hidden mode (without GUI) to generate thumbnails.

			if res_code != 0:
				raise Exception("Mirada returned bad exit-status.\\n" + mirada_path);

			#Searching for thumbnails generated by Mirada.
			#Thumbnail filename is composed from the source file name, date and time of creation - filename_yyyymmdd_hhmmss_thumb[number].jpg
			#For example: test.mov_20120305_112354_thumb1.jpg - the thumbnail of the first frame of the test.mov videofile

			thumbnails = list()
			for f in os.listdir(gen_path):
				if fnmatch.fnmatch(f, os.path.basename(filename) + '_*_thumb?.jpg'):
					thumbnails.append(gen_path + '/' + f)

			thumbnails.sort()

		Beside Mirada, some other software, for instance, ffmpeg, can be used to generate thumbnails.

		::

			#An example of generating thumbnails with ffmpeg.
			#Prior to generating thumbnails with ffmpeg it is necessary to resolve the duration of video,
			#in order to calculate the middle and the last frame correctly.
			#Let's take, for example, a 30-second long video.

			thumbnails = list() # file list for thumbnails
			thumbnails.append(filename + '_thumb1.jpg')
			thumbnails.append(filename + '_thumb2.jpg')
			thumbnails.append(filename + '_thumb3.jpg')

			subprocess.call(['ffmpeg', '-i', filename, '-s', '512x512', '-an', '-ss', '00:00:00', '-r', 1, '-vframes', 1, '-y', thumbnails[0]])
			subprocess.call(['ffmpeg', '-i', filename, '-s', '512x512', '-an', '-ss', '15:00:00', '-r', 1, '-vframes', 1, '-y', thumbnails[1]])
			subprocess.call(['ffmpeg', '-i', filename, '-s', '512x512', '-an', '-ss', '30:00:00', '-r', 1, '-vframes', 1, '-y', thumbnails[2]])
			# The key descriptions can be found in ffmpeg documentation
		"""

		if not as_link and not os.path.exists(filename):
			raise Exception('File ' + filename + ' not found')

		"""
		Attaching a file to a message goes in two phases:
		1 - adding the file to Cargador-controlled file storage
			the phase is skipped, if the file is being attached as a link
		2 - adding a corresponding entry(-ies) about the file(-s) to the database
		"""

		q_sites = ('select "listMessageUploadStores_json"(?::bigint,0)', [message_id])
		q_url = ('select "getUrlBody_byTaskId_00"((select taskid from "eventQuery_08"(?::bigint[])))', [to_array(message_id)])
		q_attach = ('select "getNewAttachmentGroupID"()', [])

		sites, rtask_url, rnew_attach_id = self.z_execute(
			False
			, ';'.join([q_sites[0], q_url[0], q_attach[0]])
			, *(q_sites[1] + q_url[1] + q_attach[1])
		)

		stores = []
		errors = []

		# Prepare storage when necessary
		if not as_link or (thumbnails is not None and len(thumbnails) > 0):
			thost = carga.storage().get("host") if (carga) is not None else None

			if sites and len(sites) > 0 and len(sites[0]) > 0:
				sites = json.loads(sites[0][0])
			else:
				sites = None

			if sites and len(sites.get("stores", [])) > 0:
				if not path or len(path) == 0:
					path = sites.get("path", "")

				for storage in sites.get("stores", []):
					if storage.get("localaddr") and len(storage.get("localaddr")) > 0: # local address
						local_addr = storage.get("localaddr").split(':')
						if len(local_addr) > 1 and local_addr[0]:
							store = {
								"user": ""
								, "unid": storage.get("unid")
								, "key": storage.get("storage_key")
								, "host": local_addr[0]
								, "http_port": storage.get("webport")
								, "local_port": int(local_addr[1])
								, "remote_port": storage.get("nativeport")
							}
							store_carga = cargador.Cargador(store)

							if thost == store["host"] and len(stores) > 0:
								stores.insert(0, store_carga)
							else:
								stores.append(store_carga)

					if storage.get("dns_name") and len(storage.get("dns_name")) > 0: # remote address
						store = {
							"user": ""
							, "unid": storage.get("unid")
							, "key": storage.get("storage_key")
							, "host": storage.get("dns_name")
							, "http_port": storage.get("webport")
							, "local_port": 0
							, "remote_port": storage.get("nativeport")
						}
						store_carga = cargador.Cargador(store)

						if self.proxy and len(self.proxy) > 0:
							store_carga.set_proxy(self.proxy)

						if thost == store["host"] and len(stores) > 0:
							stores.insert(0, store_carga)
						else:
							stores.append(store_carga)
			elif (carga) is not None:
				stores.append(carga)

		# If the file is attached physically, adding it to Cargador and getting its hash
		hash = None
		if not as_link:
			if len(stores) == 0:
				raise Exception("Cargador connection is NOT established")

			# Query to get text locator of the task
			if not path or len(path) == 0:
				path = rtask_url[0][0]

			hash = None
			for store in stores:
				try:
					hash64 = store.upload(filename, path)
					if hash64:
						hash = hash64_16(hash64)
						if len(stores) > 1:
							stores.remove(store)
							stores.insert(0, store)
						break
				except Exception as err:
					sdata = store.storage()
					errors.append("{}:{}: {}".format(sdata.get("host"), str(sdata.get("http_port")), err))

			if not hash and len(errors) > 0:
				raise Exception("{}".format("{} file upload failed.\nAttempts:\n{}".format(filename, '\n'.join(errors))))

		# Attachment properties
		tag = ATTACHMENT_TAG_FILE
		file_size = 0
		file_name = ""

		if as_link:
			tag = ATTACHMENT_TAG_LINK
			file_name = filename.replace('\\', '/')
		else:
			file_size = os.stat(filename).st_size
			file_name = os.path.basename(filename)

		"""
		The "tag" parameter is required to define the type of attachment:
		dbtypes.ATTACHMENT_TAG_FILE - file is imported, i.e., it is stored in the Cargador-controlled file storage and it has a hash sum
		dbtypes.ATTACHMENT_TAG_LINK - file/folder is linked.

		file_size - file size, bytes. For link = 0.

		file_name - file name. For a link - full path to file, for an imported file - just its basic name

		description - text comments to the attachment
		"""

		# Getting a new entry ID to enlist the file into the database
		new_attach_id = rnew_attach_id[0][0]

		# Adding the entry
		self.z_execute(False, 'select "attachNew_01"(?::bigint, ?::integer, ?, ?::integer, ?::bigint, ?, ?, ?::integer)',
				message_id, new_attach_id, hash, tag, file_size, file_name, description, flags)

		"""
		The added file may have several entries, e.g., it may have thumbnails or reviews.
		The additional entries must use the same ID but the tags must differ.
		If you modify this script for adding several files,
		you have to get a separate new ID for each file.
		"""

		if thumbnails is not None and len(thumbnails) > 0:
			if len(stores) > 0:
				# Adding thumbnails to the file storage and getting their hash sums
				hashthumbs = []
				for store in stores:
					try:
						for i in range(min(3, len(thumbnails))):
							hash64 = store.upload(thumbnails[i], u"thumbnail.cache/" + datetime.datetime.now().strftime("%Y-%m-%d"))
							if hash64: hashthumbs.append(hash64_16(hash64))

						if len(hashthumbs) > 0: break
					except Exception as err:
						print(err)

				# Adding thumbnail hash sums to the database
				if len(hashthumbs) > 0:
					q_thumb_add = 'select "newAtachment_00_"(?::bigint, ?::integer, ?, ?::integer, ?::bigint, ?, ?)'
					q_args = []

					for i in range(min(3, len(hashthumbs))):
						# Setting a tag for the thumbnail. 1 - first frame, 2 - middle frame, 3 - last frame
						tag = i+1
						# Such parameters as file size, file name and text comments make no sense, therefore the corresponding fields are empty
						q_args.extend([message_id, new_attach_id, hashthumbs[i], tag, 0, "", ""])

					# Adding thumbnail entry, executing entry adding query
					self.z_execute(False, ';'.join([ q_thumb_add for i in range(len(hashthumbs)) ]), *q_args)
			else:
				print("Uploading thumbnails: Cargador connection is NOT established")

		return new_attach_id

	def project_tags(self, project_id):
		"""
		:param int project_id: project ID.
		:returns: project tags table. The table contains all of the tags that can be set on project tasks.

		The table fields are described in the module dbtypes: :py:const:`TAG_DATA_...<py_cerebro.dbtypes.TAG_DATA_>`
		"""
		return self.z_execute(True, 'select * from "tagSchemaList_byPrj"(?::bigint)', project_id)

	def tag_enums(self, tag_id):
		"""
		:param int tag_id: tag ID.
		:returns: tag enumeration table. The table contains enumerations that can be set as the tag value.

		The table fields are described in the module dbtypes: :py:const:`TAG_ENUM_DATA_...<py_cerebro.dbtypes.TAG_ENUM_DATA_>`
		"""
		return self.z_execute(True, 'select * from "tagEnumList"(?::bigint, false)', tag_id)

	def task_set_tag_enum(self, task_id, enum_id, is_set):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param enum_id: enumeration ID(s).
		:type enum_id: int, set(int, ) or list(int, )
		:param bool is_set: enumeration set / removed parameter.

		Sets or removes tag value of enumeration or multiple enumeration type for a task(s).
		"""

		tasks = self.z_execute(False, 'select "tagTaskSetEnum_a"(?::bigint[],?::bigint[],?)', to_array(enum_id), to_array(task_id), is_set)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_tag_float(self, task_id, tag_id, value):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param int tag_id: tag ID.
		:param int value: tag value.

		Sets tag value of floating-point number type for a task(s).
		"""

		tasks = self.z_execute(False, 'select "tagTaskSetReal_a"(?::bigint,?::bigint[],?)', tag_id, to_array(task_id), value)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_tag_int(self, task_id, tag_id, value):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param int tag_id: tag ID.
		:param int value: tag value.

		Sets tag value of integer number type for a task(s).
		"""

		tasks = self.z_execute(False, 'select "tagTaskSetInt_a"(?::bigint,?::bigint[],?)', tag_id, to_array(task_id), value)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_tag_string(self, task_id, tag_id, value):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param int tag_id: tag ID.
		:param int value: tag value.

		Sets tag value of string type for a task(s).
		"""

		tasks = self.z_execute(False, 'select "tagTaskSetStr_a"(?::bigint,?::bigint[],?)', tag_id, to_array(task_id), value)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_set_tag_user(self, task_id, tag_id, user_id, is_set):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param int tag_id: tag ID.
		:param user_id: user ID(s).
		:type user_id: int, set(int, ) or list(int, )
		:param bool is_set: enumeration set / removed parameter.

		Sets or removes tag value of user or multiple users type for a task(s).
		"""

		tasks = self.z_execute(False, 'select "tagTaskSetUser_a"(?::bigint,?,?::bigint[],?)', tag_id, to_array(user_id), to_array(task_id), is_set)

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_tag_reset(self, task_id, tag_id):
		"""
		:param task_id: task ID(s).
		:type task_id: int, set(int, ) or list(int, )
		:param int tag_id: tag ID.

		Removes tag value from a task(s).
		"""

		tasks = self.z_execute(False, 'select "tagTaskReset_a"(?::bigint,?::bigint[])', tag_id, to_array(task_id))

		ids = set()
		for task in tasks:
			ids.add(task[0])

		return ids

	def task_tag_enums(self, task_id, tag_id):
		"""
		:param int task_id: task ID.
		:param int tag_id: tag ID.
		:returns: table of tag enumerations set on a task.

		The table fields are described in the module dbtypes: :py:const:`TASK_TAG_ENUM_...<py_cerebro.dbtypes.TASK_TAG_ENUM_>`
		"""
		return self.z_execute(True, 'select * from "tagTaskEnums"(?::bigint,?::bigint)', task_id, tag_id)

	def task_tags(self, task_id):
		"""
		:param int task_id: task ID.
		:returns: table of tag values set on a task.

		The table fields are described in the module dbtypes: :py:const:`TASK_TAG_DATA_...<py_cerebro.dbtypes.TASK_TAG_DATA_>`
		"""
		return self.z_execute(True, 'select * from "tagTaskList"(?::bigint)', task_id)

	def task_by_url(self, url):
		"""
		:param str url: url of task.
		:returns: task ID.

		Return task ID by url. Url example: '/Test project/test'
		"""
		id = self.z_execute(True, 'select * from "getTaskID_byURL"(?)', url)
		if len(id) == 1:
			return id[0]

		return None
