mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-24 09:40:21 -06:00
542 lines
18 KiB
ReStructuredText
542 lines
18 KiB
ReStructuredText
Code Snippet
|
|
------------
|
|
|
|
|
|
This document contains code for some of the important classes, listed as
|
|
below:
|
|
|
|
* PgAdminModule_
|
|
* NodeView_
|
|
* BaseDriver_
|
|
* BaseConnection_
|
|
|
|
|
|
.. _PgAdminModule:
|
|
|
|
PgAdminModule
|
|
*************
|
|
|
|
|
|
PgAdminModule is inherted from Flask.Blueprint module.
|
|
This module defines a set of methods, properties and attributes, that every module should implement.
|
|
|
|
|
|
.. code-block:: python
|
|
|
|
class PgAdminModule(Blueprint):
|
|
"""
|
|
Base class for every PgAdmin Module.
|
|
|
|
This class defines a set of method and attributes that
|
|
every module should implement.
|
|
"""
|
|
|
|
def __init__(self, name, import_name, **kwargs):
|
|
kwargs.setdefault('url_prefix', '/' + name)
|
|
kwargs.setdefault('template_folder', 'templates')
|
|
kwargs.setdefault('static_folder', 'static')
|
|
self.submodules = []
|
|
super(PgAdminModule, self).__init__(name, import_name, **kwargs)
|
|
|
|
def register(self, app, options, first_registration=False):
|
|
"""
|
|
Override the default register function to automagically register
|
|
sub-modules at once.
|
|
"""
|
|
if first_registration:
|
|
self.submodules = list(app.find_submodules(self.import_name))
|
|
super(PgAdminModule, self).register(app, options, first_registration)
|
|
for module in self.submodules:
|
|
app.register_blueprint(module)
|
|
|
|
def get_own_stylesheets(self):
|
|
"""
|
|
Returns:
|
|
list: the stylesheets used by this module, not including any
|
|
stylesheet needed by the submodules.
|
|
"""
|
|
return []
|
|
|
|
def get_own_javascripts(self):
|
|
"""
|
|
Returns:
|
|
list: the javascripts used by this module, not including
|
|
any script needed by the submodules.
|
|
"""
|
|
return []
|
|
|
|
def get_own_menuitems(self):
|
|
"""
|
|
Returns:
|
|
dict: the menuitems for this module, not including
|
|
any needed from the submodules.
|
|
"""
|
|
return defaultdict(list)
|
|
|
|
def get_panels(self):
|
|
"""
|
|
Returns:
|
|
list: a list of panel objects to add
|
|
"""
|
|
return []
|
|
|
|
@property
|
|
def stylesheets(self):
|
|
stylesheets = self.get_own_stylesheets()
|
|
for module in self.submodules:
|
|
stylesheets.extend(module.stylesheets)
|
|
return stylesheets
|
|
|
|
@property
|
|
def javascripts(self):
|
|
javascripts = self.get_own_javascripts()
|
|
for module in self.submodules:
|
|
javascripts.extend(module.javascripts)
|
|
return javascripts
|
|
|
|
@property
|
|
def menu_items(self):
|
|
menu_items = self.get_own_menuitems()
|
|
for module in self.submodules:
|
|
for key, value in module.menu_items.items():
|
|
menu_items[key].extend(value)
|
|
menu_items = dict((key, sorted(value, key=attrgetter('priority')))
|
|
for key, value in menu_items.items())
|
|
return menu_items
|
|
|
|
|
|
.. _NodeView:
|
|
|
|
NodeView
|
|
********
|
|
|
|
|
|
NodeView class helps exposing basic REST APIs for different operations used by
|
|
pgAdmin Browser. The basic idea has been taken from the `Flask's MethodView
|
|
<http://flask.pocoo.org/docs/0.10/api/#flask.views.MethodView>`_ class. Because
|
|
- we need a lot more operations (not, just CRUD), we can not use it directly.
|
|
|
|
.. code-block:: python
|
|
|
|
class NodeView(with_metaclass(MethodViewType, View)):
|
|
"""
|
|
A PostgreSQL Object has so many operaions/functions apart from CRUD
|
|
(Create, Read, Update, Delete):
|
|
i.e.
|
|
- Reversed Engineered SQL
|
|
- Modified Query for parameter while editing object attributes
|
|
i.e. ALTER TABLE ...
|
|
- Statistics of the objects
|
|
- List of dependents
|
|
- List of dependencies
|
|
- Listing of the children object types for the certain node
|
|
It will used by the browser tree to get the children nodes
|
|
|
|
This class can be inherited to achieve the diffrent routes for each of the
|
|
object types/collections.
|
|
|
|
OPERATION | URL | HTTP Method | Method
|
|
---------------+-----------------------------+-------------+--------------
|
|
List | /obj/[Parent URL]/ | GET | list
|
|
Properties | /obj/[Parent URL]/id | GET | properties
|
|
Create | /obj/[Parent URL]/ | POST | create
|
|
Delete | /obj/[Parent URL]/id | DELETE | delete
|
|
Update | /obj/[Parent URL]/id | PUT | update
|
|
|
|
SQL (Reversed | /sql/[Parent URL]/id | GET | sql
|
|
Engineering) |
|
|
SQL (Modified | /msql/[Parent URL]/id | GET | modified_sql
|
|
Properties) |
|
|
|
|
Statistics | /stats/[Parent URL]/id | GET | statistics
|
|
Dependencies | /dependency/[Parent URL]/id | GET | dependencies
|
|
Dependents | /dependent/[Parent URL]/id | GET | dependents
|
|
|
|
Nodes | /nodes/[Parent URL]/ | GET | nodes
|
|
Current Node | /nodes/[Parent URL]/id | GET | node
|
|
|
|
Children | /children/[Parent URL]/id | GET | children
|
|
|
|
NOTE:
|
|
Parent URL can be seen as the path to identify the particular node.
|
|
|
|
i.e.
|
|
In order to identify the TABLE object, we need server -> database -> schema
|
|
information.
|
|
"""
|
|
operations = dict({
|
|
'obj': [
|
|
{'get': 'properties', 'delete': 'delete', 'put': 'update'},
|
|
{'get': 'list', 'post': 'create'}
|
|
],
|
|
'nodes': [{'get': 'node'}, {'get': 'nodes'}],
|
|
'sql': [{'get': 'sql'}],
|
|
'msql': [{'get': 'modified_sql'}],
|
|
'stats': [{'get': 'statistics'}],
|
|
'dependency': [{'get': 'dependencies'}],
|
|
'dependent': [{'get': 'dependents'}],
|
|
'children': [{'get': 'children'}],
|
|
'module.js': [{}, {}, {'get': 'module_js'}]
|
|
})
|
|
|
|
@classmethod
|
|
def generate_ops(cls):
|
|
cmds = []
|
|
for op in cls.operations:
|
|
idx = 0
|
|
for ops in cls.operations[op]:
|
|
meths = []
|
|
for meth in ops:
|
|
meths.append(meth.upper())
|
|
if len(meths) > 0:
|
|
cmds.append({
|
|
'cmd': op, 'req': (idx == 0),
|
|
'with_id': (idx != 2), 'methods': meths
|
|
})
|
|
idx += 1
|
|
return cmds
|
|
|
|
# Inherited class needs to modify these parameters
|
|
node_type = None
|
|
# This must be an array object with attributes (type and id)
|
|
parent_ids = []
|
|
# This must be an array object with attributes (type and id)
|
|
ids = []
|
|
|
|
@classmethod
|
|
def get_node_urls(cls):
|
|
assert cls.node_type is not None, \
|
|
"Please set the node_type for this class ({0})".format(
|
|
str(cls.__class__.__name__))
|
|
common_url = '/'
|
|
for p in cls.parent_ids:
|
|
common_url += '<{0}:{1}>/'.format(str(p['type']), str(p['id']))
|
|
|
|
id_url = None
|
|
for p in cls.ids:
|
|
id_url = '{0}<{1}:{2}>'.format(common_url if not id_url else id_url,
|
|
p['type'], p['id'])
|
|
|
|
return id_url, common_url
|
|
|
|
def __init__(self, **kwargs):
|
|
self.cmd = kwargs['cmd']
|
|
|
|
# Check the existance of all the required arguments from parent_ids
|
|
# and return combination of has parent arguments, and has id arguments
|
|
def check_args(self, **kwargs):
|
|
has_id = has_args = True
|
|
for p in self.parent_ids:
|
|
if p['id'] not in kwargs:
|
|
has_args = False
|
|
break
|
|
|
|
for p in self.ids:
|
|
if p['id'] not in kwargs:
|
|
has_id = False
|
|
break
|
|
|
|
return has_args, has_id and has_args
|
|
|
|
def dispatch_request(self, *args, **kwargs):
|
|
meth = flask.request.method.lower()
|
|
if meth == 'head':
|
|
meth = 'get'
|
|
|
|
assert self.cmd in self.operations, \
|
|
"Unimplemented Command ({0}) for {1}".format(
|
|
self.cmd,
|
|
str(self.__class__.__name__)
|
|
)
|
|
|
|
has_args, has_id = self.check_args(**kwargs)
|
|
|
|
assert (self.cmd in self.operations and
|
|
(has_id and len(self.operations[self.cmd]) > 0 and
|
|
meth in self.operations[self.cmd][0]) or
|
|
(not has_id and len(self.operations[self.cmd]) > 1 and
|
|
meth in self.operations[self.cmd][1]) or
|
|
(len(self.operations[self.cmd]) > 2 and
|
|
meth in self.operations[self.cmd][2])), \
|
|
"Unimplemented method ({0}) for command ({1}), which {2} an id".format(
|
|
meth, self.cmd,
|
|
'requires' if has_id else 'does not require'
|
|
)
|
|
|
|
meth = self.operations[self.cmd][0][meth] if has_id else \
|
|
self.operations[self.cmd][1][meth] if has_args and \
|
|
meth in self.operations[self.cmd][1] else \
|
|
self.operations[self.cmd][2][meth]
|
|
|
|
method = getattr(self, meth, None)
|
|
|
|
if method is None:
|
|
return make_json_response(
|
|
status=406,
|
|
success=0,
|
|
errormsg=gettext(
|
|
"Unimplemented method ({0}) for this url ({1})".format(
|
|
meth, flask.request.path)
|
|
)
|
|
)
|
|
|
|
return method(*args, **kwargs)
|
|
|
|
@classmethod
|
|
def register_node_view(cls, blueprint):
|
|
cls.blueprint = blueprint
|
|
id_url, url = cls.get_node_urls()
|
|
|
|
commands = cls.generate_ops()
|
|
|
|
for c in commands:
|
|
if c['with_id']:
|
|
blueprint.add_url_rule(
|
|
'/{0}{1}'.format(
|
|
c['cmd'], id_url if c['req'] else url
|
|
),
|
|
view_func=cls.as_view(
|
|
'{0}{1}'.format(
|
|
c['cmd'], '_id' if c['req'] else ''
|
|
),
|
|
cmd=c['cmd']
|
|
),
|
|
methods=c['methods']
|
|
)
|
|
else:
|
|
blueprint.add_url_rule(
|
|
'/{0}'.format(c['cmd']),
|
|
view_func=cls.as_view(
|
|
'{0}'.format(c['cmd']), cmd=c['cmd']
|
|
),
|
|
methods=c['methods']
|
|
)
|
|
|
|
def module_js(self, **kwargs):
|
|
"""
|
|
This property defines (if javascript) exists for this node.
|
|
Override this property for your own logic.
|
|
"""
|
|
return flask.make_response(
|
|
flask.render_template(
|
|
"{0}/js/{0}.js".format(self.node_type)
|
|
),
|
|
200, {'Content-Type': 'application/x-javascript'}
|
|
)
|
|
|
|
def children(self, *args, **kwargs):
|
|
"""Build a list of treeview nodes from the child nodes."""
|
|
children = []
|
|
|
|
for module in self.blueprint.submodules:
|
|
children.extend(module.get_nodes(*args, **kwargs))
|
|
|
|
return make_json_response(data=children)
|
|
|
|
|
|
.. _BaseDriver:
|
|
|
|
BaseDriver
|
|
**********
|
|
|
|
.. code-block:: python
|
|
|
|
class BaseDriver(object):
|
|
"""
|
|
class BaseDriver(object):
|
|
|
|
This is a base class for different server types.
|
|
Inherit this class to implement different type of database driver
|
|
implementation.
|
|
|
|
(For PostgreSQL/Postgres Plus Advanced Server, we will be using psycopg2)
|
|
|
|
Abstract Properties:
|
|
-------- ----------
|
|
* Version (string):
|
|
Current version string for the database server
|
|
|
|
Abstract Methods:
|
|
-------- -------
|
|
* get_connection(*args, **kwargs)
|
|
- It should return a Connection class object, which may/may not be
|
|
connected to the database server.
|
|
|
|
* release_connection(*args, **kwargs)
|
|
- Implement the connection release logic
|
|
|
|
* gc()
|
|
- Implement this function to release the connections assigned in the
|
|
session, which has not been pinged from more than the idle timeout
|
|
configuration.
|
|
"""
|
|
|
|
@abstractproperty
|
|
def Version(cls):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_connection(self, *args, **kwargs):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def release_connection(self, *args, **kwargs):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def gc(self):
|
|
pass
|
|
|
|
|
|
.. _BaseConnection:
|
|
|
|
BaseConnection
|
|
**************
|
|
|
|
.. code-block:: python
|
|
|
|
class BaseConnection(object):
|
|
"""
|
|
class BaseConnection(object)
|
|
|
|
It is a base class for database connection. A different connection
|
|
drive must implement this to expose abstract methods for this server.
|
|
|
|
General idea is to create a wrapper around the actual driver
|
|
implementation. It will be instantiated by the driver factory
|
|
basically. And, they should not be instantiated directly.
|
|
|
|
|
|
Abstract Methods:
|
|
-------- -------
|
|
* connect(**kwargs)
|
|
- Define this method to connect the server using that particular driver
|
|
implementation.
|
|
|
|
* execute_scalar(query, params)
|
|
- Implement this method to execute the given query and returns single
|
|
datum result.
|
|
|
|
* execute_async(query, params)
|
|
- Implement this method to execute the given query asynchronously and returns result.
|
|
|
|
* execute_void(query, params)
|
|
- Implement this method to execute the given query with no result.
|
|
|
|
* execute_2darray(query, params)
|
|
- Implement this method to execute the given query and returns the result
|
|
as a 2 dimensional array.
|
|
|
|
* execute_dict(query, params)
|
|
- Implement this method to execute the given query and returns the result
|
|
as an array of dict (column name -> value) format.
|
|
|
|
* connected()
|
|
- Implement this method to get the status of the connection. It should
|
|
return True for connected, otherwise False
|
|
|
|
* reset()
|
|
- Implement this method to reconnect the database server (if possible)
|
|
|
|
* transaction_status()
|
|
- Implement this method to get the transaction status for this
|
|
connection. Range of return values different for each driver type.
|
|
|
|
* ping()
|
|
- Implement this method to ping the server. There are times, a connection
|
|
has been lost, but - the connection driver does not know about it. This
|
|
can be helpful to figure out the actual reason for query failure.
|
|
|
|
* _release()
|
|
- Implement this method to release the connection object. This should not
|
|
be directly called using the connection object itself.
|
|
|
|
NOTE: Please use BaseDriver.release_connection(...) for releasing the
|
|
connection object for better memory management, and connection pool
|
|
management.
|
|
|
|
* _wait(conn)
|
|
- Implement this method to wait for asynchronous connection to finish the
|
|
execution, hence - it must be a blocking call.
|
|
|
|
* _wait_timeout(conn, time)
|
|
- Implement this method to wait for asynchronous connection with timeout.
|
|
This must be a non blocking call.
|
|
|
|
* poll()
|
|
- Implement this method to poll the data of query running on asynchronous
|
|
connection.
|
|
|
|
* cancel_transaction(conn_id, did=None)
|
|
- Implement this method to cancel the running transaction.
|
|
|
|
* messages()
|
|
- Implement this method to return the list of the messages/notices from
|
|
the database server.
|
|
"""
|
|
|
|
ASYNC_OK = 1
|
|
ASYNC_READ_TIMEOUT = 2
|
|
ASYNC_WRITE_TIMEOUT = 3
|
|
ASYNC_NOT_CONNECTED = 4
|
|
|
|
@abstractmethod
|
|
def connect(self, **kwargs):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def execute_scalar(self, query, params=None):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def execute_async(self, query, params=None):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def execute_void(self, query, params=None):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def execute_2darray(self, query, params=None):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def execute_dict(self, query, params=None):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def connected(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def reset(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def transaction_status(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def ping(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def _release(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def _wait(self, conn):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def _wait_timeout(self, conn, time):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def poll(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def cancel_transaction(self, conn_id, did=None):
|
|
pass
|