mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Update document for the develpers as per current implementation
This commit is contained in:
484
docs/en_US/code-snippet.rst
Normal file
484
docs/en_US/code-snippet.rst
Normal file
@@ -0,0 +1,484 @@
|
||||
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 = {key: sorted(values, key=attrgetter('priority'))
|
||||
for key, values 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 | Method
|
||||
---------------+------------------------+--------
|
||||
List | /obj/[Parent URL]/ | GET
|
||||
Properties | /obj/[Parent URL]/id | GET
|
||||
Create | /obj/[Parent URL]/ | POST
|
||||
Delete | /obj/[Parent URL]/id | DELETE
|
||||
Update | /obj/[Parent URL]/id | PUT
|
||||
|
||||
SQL (Reversed | /sql/[Parent URL]/id | GET
|
||||
Engineering) |
|
||||
SQL (Modified | /sql/[Parent URL]/id | POST
|
||||
Properties) |
|
||||
|
||||
Statistics | /stats/[Parent URL]/id | GET
|
||||
Dependencies | /deps/[Parent URL]/id | GET
|
||||
Dependents | /deps/[Parent URL]/id | POST
|
||||
|
||||
Children Nodes | /nodes/[Parent URL]/id | GET
|
||||
|
||||
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': 'nodes'}],
|
||||
'sql': [{'get': 'sql', 'post': 'modified_sql'}],
|
||||
'stats': [{'get': 'statistics'}],
|
||||
'deps': [{'get': 'dependencies', 'post': 'dependents'}],
|
||||
'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}/{0}.js".format(self.node_type)
|
||||
),
|
||||
200, {'Content-Type': 'application/x-javascript'}
|
||||
)
|
||||
|
||||
def nodes(self, *args, **kwargs):
|
||||
"""Build a list of treeview nodes from the child nodes."""
|
||||
nodes = []
|
||||
|
||||
for module in self.blueprint.submodules:
|
||||
nodes.extend(module.get_nodes(*args, **kwargs))
|
||||
|
||||
return make_json_response(data=nodes)
|
||||
|
||||
|
||||
.. _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.
|
||||
"""
|
||||
__metaclass__ = DriverRegistry
|
||||
|
||||
@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 actaul 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_2darray(query, params)
|
||||
- Implement this method to execute the given query and returns the result
|
||||
as a 2 dimentional 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.
|
||||
"""
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def connect(self, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def execute_scalar(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
|
||||
Reference in New Issue
Block a user