Object Inheritance for PGSMO (#43)
* WIP * Implementation to utilize the node query helper * Adding base class for node objects to inherit from * Adding node collection class that does lazy loading of child objects * Making role and tablespaces inherit from NodeObject * Adding unittests and some fixes as per the unittests * Adding unit tst for get_nodes * Fixes as per code review comments and flake8 stuffs * Fixing small bug failing tests?
This commit is contained in:
Родитель
8dece99b43
Коммит
04168d6f09
|
@ -3,66 +3,51 @@
|
|||
# Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import pgsmo.objects.node_object as node
|
||||
import pgsmo.utils as utils
|
||||
|
||||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
||||
|
||||
|
||||
class Column:
|
||||
@staticmethod
|
||||
def get_columns_for_table(conn: utils.querying.ConnectionWrapper, tid: int, fetch: bool=False):
|
||||
# Execute query to get list of columns
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(TEMPLATE_ROOT, 'nodes.sql', conn.version),
|
||||
tid=tid
|
||||
# TODO: Add show system objs support
|
||||
)
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [Column._from_node_query(conn, row['oid'], row['name'], row['datatype'], **row) for row in rows]
|
||||
class Column(node.NodeObject):
|
||||
@classmethod
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper, tid: int) -> List['Column']:
|
||||
return node.get_nodes(conn, TEMPLATE_ROOT, cls._from_node_query, tid=tid)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper,
|
||||
col_id: int, col_name: str, col_datatype: str,
|
||||
**kwargs):
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'Column':
|
||||
"""
|
||||
Creates a new Column object based on the the results from the column nodes query
|
||||
:param conn: Connection used to execute the column nodes query
|
||||
:param kwargs: Optional parameters for the column
|
||||
:params col_id: Object ID of the column
|
||||
:params col_name: Name of the column
|
||||
:params col_datatype: Type of the column
|
||||
Kwargs:
|
||||
name str: Name of the column
|
||||
datatype str: Name of the type of the column
|
||||
oid int: Object ID of the column
|
||||
not_null bool: Whether or not null is allowed for the column
|
||||
has_default_value bool: Whether or not the column has a default value constraint
|
||||
:return: Instance of the Column
|
||||
"""
|
||||
col = cls(col_name, col_datatype)
|
||||
|
||||
# Assign the mandatory properties
|
||||
col._cid = col_id
|
||||
col._conn = conn
|
||||
|
||||
# Assign the optional properties
|
||||
col._has_default_value = kwargs.get('has_default_value')
|
||||
col._not_null = kwargs.get('not_null')
|
||||
col = cls(conn, kwargs['name'], kwargs['datatype'])
|
||||
col._oid = kwargs['oid']
|
||||
col._has_default_value = kwargs['has_default_val']
|
||||
col._not_null = kwargs['not_null']
|
||||
|
||||
return col
|
||||
|
||||
def __init__(self, name: str, datatype: str):
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str, datatype: str):
|
||||
"""
|
||||
Initializes a new instance of a Column
|
||||
:param conn: Connection to the server/database that this object will belong to
|
||||
:param name: Name of the column
|
||||
:param datatype: Type of the column
|
||||
"""
|
||||
self._name: str = name
|
||||
super(Column, self).__init__(conn, name)
|
||||
self._datatype: str = datatype
|
||||
|
||||
# Declare the optional parameters
|
||||
self._conn: Optional[utils.querying.ConnectionWrapper] = None
|
||||
self._cid: Optional[int] = None
|
||||
self._has_default_value: Optional[bool] = None
|
||||
self._not_null: Optional[bool] = None
|
||||
|
||||
|
@ -75,18 +60,10 @@ class Column:
|
|||
def has_default_value(self) -> Optional[bool]:
|
||||
return self._has_default_value
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def not_null(self) -> Optional[bool]:
|
||||
return self._not_null
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
return self._cid
|
||||
|
||||
# METHODS ##############################################################
|
||||
def refresh(self):
|
||||
self._fetch_properties()
|
||||
|
|
|
@ -5,77 +5,59 @@
|
|||
|
||||
from typing import List, Optional # noqa
|
||||
|
||||
import pgsmo.objects.schema.schema as schema
|
||||
import pgsmo.objects.node_object as node
|
||||
from pgsmo.objects.schema.schema import Schema
|
||||
import pgsmo.utils as utils
|
||||
|
||||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
||||
|
||||
|
||||
class Database:
|
||||
@staticmethod
|
||||
def get_databases_for_server(conn: utils.querying.ConnectionWrapper, fetch: bool=True) -> List['Database']:
|
||||
# Execute query to get list of databases
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(TEMPLATE_ROOT, 'nodes.sql', conn.version),
|
||||
last_system_oid=0
|
||||
)
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [Database._from_node_query(conn, row['did'], row['name'], fetch, **row) for row in rows]
|
||||
class Database(node.NodeObject):
|
||||
@classmethod
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper) -> List['Database']:
|
||||
return node.get_nodes(conn, TEMPLATE_ROOT, cls._from_node_query, last_system_oid=0)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, db_did: int, db_name: str, fetch: bool=True,
|
||||
**kwargs):
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'Database':
|
||||
"""
|
||||
Creates a new Database object based on the results from a query to lookup databases
|
||||
:param conn: Connection used to generate the db info query
|
||||
:param db_did: Object ID of the database
|
||||
:param db_name: Name of the database
|
||||
:param kwargs: Optional parameters for the database. Values that can be provided:
|
||||
Kwargs:
|
||||
did int: Object ID of the database
|
||||
name str: Name of the database
|
||||
spcname str: Name of the tablespace for the database
|
||||
datallowconn bool: Whether or not the database can be connected to
|
||||
cancreate bool: Whether or not the database can be created by the current user
|
||||
owner int: Object ID of the user that owns the database
|
||||
:return: Instance of the Database
|
||||
"""
|
||||
db = cls(db_name)
|
||||
|
||||
# Assign the mandatory properties
|
||||
db._did = db_did
|
||||
db._conn = conn
|
||||
db._is_connected = db_name == conn.dsn_parameters.get('dbname')
|
||||
|
||||
# Assign the optional properties
|
||||
db._tablespace = kwargs.get('spcname')
|
||||
db._allow_conn = kwargs.get('datallowconn')
|
||||
db._can_create = kwargs.get('cancreate')
|
||||
db._owner_oid = kwargs.get('owner')
|
||||
|
||||
# If fetch was requested, do complete refresh
|
||||
if fetch and db._is_connected:
|
||||
db.refresh()
|
||||
db = cls(conn, kwargs['name'])
|
||||
db._oid = kwargs['did']
|
||||
db._is_connected = kwargs['name'] == conn.dsn_parameters.get('dbname')
|
||||
db._tablespace = kwargs['spcname']
|
||||
db._allow_conn = kwargs['datallowconn']
|
||||
db._can_create = kwargs['cancreate']
|
||||
db._owner_oid = kwargs['owner']
|
||||
|
||||
return db
|
||||
|
||||
def __init__(self, name: str):
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str):
|
||||
"""
|
||||
Initializes a new instance of a database
|
||||
:param name: Name of the database
|
||||
"""
|
||||
self._name: str = name
|
||||
super(Database, self).__init__(conn, name)
|
||||
self._is_connected: bool = False
|
||||
|
||||
# Declare the optional parameters
|
||||
self._conn: utils.querying.ConnectionWrapper = None
|
||||
self._did: Optional[int] = None
|
||||
self._tablespace: Optional[str] = None
|
||||
self._allow_conn: Optional[bool] = None
|
||||
self._can_create: Optional[bool] = None
|
||||
self._owner_oid: Optional[int] = None
|
||||
|
||||
# Declare the child items
|
||||
self._schemas: List[schema.Schema] = None
|
||||
self._schemas: node.NodeCollection = node.NodeCollection(lambda: Schema.get_nodes_for_parent(self._conn))
|
||||
|
||||
# PROPERTIES ###########################################################
|
||||
# TODO: Create setters for optional values
|
||||
|
@ -88,27 +70,20 @@ class Database:
|
|||
def can_create(self) -> bool:
|
||||
return self._can_create
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> int:
|
||||
return self._did
|
||||
|
||||
@property
|
||||
def schemas(self) -> List[schema.Schema]:
|
||||
return self._schemas
|
||||
|
||||
@property
|
||||
def tablespace(self) -> str:
|
||||
return self._tablespace
|
||||
|
||||
# -CHILD OBJECTS #######################################################
|
||||
@property
|
||||
def schemas(self) -> node.NodeCollection:
|
||||
return self._schemas
|
||||
|
||||
# METHODS ##############################################################
|
||||
|
||||
def refresh(self):
|
||||
self._fetch_properties()
|
||||
self._fetch_schemas()
|
||||
self._schemas.reset()
|
||||
|
||||
def create(self):
|
||||
pass
|
||||
|
@ -122,6 +97,3 @@ class Database:
|
|||
# IMPLEMENTATION DETAILS ###############################################
|
||||
def _fetch_properties(self):
|
||||
pass
|
||||
|
||||
def _fetch_schemas(self):
|
||||
self._schemas = schema.Schema.get_schemas_for_database(self._conn)
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
# --------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import Callable, Dict, List, Optional, Union, TypeVar
|
||||
|
||||
|
||||
import pgsmo.utils.templating as templating
|
||||
import pgsmo.utils.querying as querying
|
||||
|
||||
|
||||
class NodeObject:
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def _from_node_query(cls, conn: querying.ConnectionWrapper, **kwargs):
|
||||
pass
|
||||
|
||||
def __init__(self, conn: querying.ConnectionWrapper, name: str):
|
||||
# Define the state of the object
|
||||
self._conn: querying.ConnectionWrapper = conn
|
||||
|
||||
# Declare node basic properties
|
||||
self._name: str = name
|
||||
self._oid: Optional[int] = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
return self._oid
|
||||
|
||||
|
||||
TNC = TypeVar('TNC')
|
||||
|
||||
|
||||
class NodeCollection:
|
||||
def __init__(self, generator: Callable[[], List[TNC]]):
|
||||
"""
|
||||
Initializes a new collection of node objects.
|
||||
:param generator: A callable that returns a list of NodeObjects when called
|
||||
"""
|
||||
self._generator: Callable[[], List[TNC]] = generator
|
||||
self._items: Optional[List[NodeObject]] = None
|
||||
|
||||
def __getitem__(self, index: Union[int, str]) -> TNC:
|
||||
"""
|
||||
Searches for a node in the list of items by OID or name
|
||||
:param index: If an int, the object ID of the item to look up. If a str, the name of the
|
||||
item to look up. Otherwise, TypeError will be raised.
|
||||
:raises TypeError: If index is not a str or int
|
||||
:raises NameError: If an item with the provided index does not exist
|
||||
:return: The instance that matches the provided index
|
||||
"""
|
||||
# Determine how we will be looking up the item
|
||||
if isinstance(index, int):
|
||||
# Lookup is by object ID
|
||||
lookup = (lambda x: x.oid == index)
|
||||
elif isinstance(index, str):
|
||||
# Lookup is by object name
|
||||
lookup = (lambda x: x.name == index)
|
||||
else:
|
||||
raise TypeError('Index must be either a string or int')
|
||||
|
||||
# Load the items if they haven't been loaded
|
||||
if self._items is None:
|
||||
self._items = self._generator()
|
||||
|
||||
# Look up the desired item
|
||||
for item in self._items:
|
||||
if lookup(item):
|
||||
return item
|
||||
|
||||
# If we make it to here, an item with the given index does not exist
|
||||
raise NameError('An item with the provided index does not exist')
|
||||
|
||||
def __iter__(self):
|
||||
# Load the items if they haven't been loaded
|
||||
if self._items is None:
|
||||
self._items = self._generator()
|
||||
|
||||
return self._items.__iter__()
|
||||
|
||||
def reset(self) -> None:
|
||||
# Empty the items so that next iteration will reload the collection
|
||||
self._items = None
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def get_nodes(conn: querying.ConnectionWrapper,
|
||||
template_root: str,
|
||||
generator: Callable[[type, querying.ConnectionWrapper, Dict[str, any]], T],
|
||||
**kwargs) -> List[T]:
|
||||
"""
|
||||
Renders and executes nodes.sql for the given database version to generate a list of NodeObjects
|
||||
:param conn: Connection to use to execute the nodes query
|
||||
:param template_root: Root directory of the templates
|
||||
:param generator: Callable to execute with a row from the nodes query to generate the NodeObject
|
||||
:param kwargs: Optional parameters provided as the context for rendering the template
|
||||
:return: A NodeObject generated by the generator
|
||||
"""
|
||||
sql = templating.render_template(
|
||||
templating.get_template_path(template_root, 'nodes.sql', conn.version),
|
||||
**kwargs
|
||||
)
|
||||
cols, rows = querying.execute_dict(conn, sql)
|
||||
|
||||
return [generator(conn, **row) for row in rows]
|
|
@ -5,26 +5,22 @@
|
|||
|
||||
from typing import List, Optional
|
||||
|
||||
from pgsmo.objects.node_object import NodeObject, get_nodes
|
||||
import pgsmo.utils as utils
|
||||
|
||||
|
||||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
||||
|
||||
|
||||
class Role:
|
||||
class Role(NodeObject):
|
||||
@classmethod
|
||||
def get_roles_for_server(cls, conn: utils.querying.ConnectionWrapper) -> List['Role']:
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper) -> List['Role']:
|
||||
"""
|
||||
Generates a list of roles for a given server. Intended to only be called by a Server object
|
||||
:param conn: Connection to use to look up the roles for the server
|
||||
:return: List of Role objects
|
||||
"""
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(TEMPLATE_ROOT, 'nodes.sql', conn.version),
|
||||
)
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [cls._from_node_query(conn, **row) for row in rows]
|
||||
return get_nodes(conn, TEMPLATE_ROOT, cls._from_node_query)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'Role':
|
||||
|
@ -34,24 +30,24 @@ class Role:
|
|||
:param kwargs: Row from a role node query
|
||||
:return: A Role instnace
|
||||
"""
|
||||
role = cls()
|
||||
role._conn = conn
|
||||
role = cls(conn, kwargs['rolname'])
|
||||
|
||||
# Define values from node query
|
||||
role._oid = kwargs.get('oid')
|
||||
role._name = kwargs.get('rolname')
|
||||
role._can_login = kwargs.get('rolcanlogin')
|
||||
role._super = kwargs.get('rolsuper')
|
||||
role._oid = kwargs['oid']
|
||||
role._can_login = kwargs['rolcanlogin']
|
||||
role._super = kwargs['rolsuper']
|
||||
|
||||
return role
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes internal state of a Role object"""
|
||||
self._conn: Optional[utils.querying.ConnectionWrapper] = None
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str):
|
||||
"""
|
||||
Initializes internal state of a Role object
|
||||
:param conn: Connection that executed the role node query
|
||||
:param name: Name of the role
|
||||
"""
|
||||
super(Role, self).__init__(conn, name)
|
||||
|
||||
# Declare basic properties
|
||||
self._oid: Optional[int] = None
|
||||
self._name: Optional[str] = None
|
||||
self._can_login: Optional[bool] = None
|
||||
self._super: Optional[bool] = None
|
||||
|
||||
|
@ -62,16 +58,6 @@ class Role:
|
|||
"""Whether or not the role can login to the server"""
|
||||
return self._can_login
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""Name of the role"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
"""Object ID of the role"""
|
||||
return self._oid
|
||||
|
||||
@property
|
||||
def super(self) -> Optional[bool]:
|
||||
"""Whether or not the role is a super user"""
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import os.path as path
|
||||
from typing import List, Optional
|
||||
|
||||
import pgsmo.objects.node_object as node
|
||||
from pgsmo.objects.table.table import Table
|
||||
from pgsmo.objects.view.view import View
|
||||
import pgsmo.utils as utils
|
||||
|
@ -14,51 +15,46 @@ import pgsmo.utils as utils
|
|||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
||||
|
||||
|
||||
class Schema:
|
||||
@staticmethod
|
||||
def get_schemas_for_database(conn: utils.querying.ConnectionWrapper) -> List['Schema']:
|
||||
class Schema(node.NodeObject):
|
||||
@classmethod
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper) -> List['Schema']:
|
||||
type_template_root = path.join(TEMPLATE_ROOT, conn.server_type)
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(type_template_root, 'nodes.sql', conn.version),
|
||||
|
||||
)
|
||||
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [Schema._from_node_query(conn, row['oid'], row['name'], **row) for row in rows]
|
||||
return node.get_nodes(conn, type_template_root, cls._from_node_query)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn, schema_oid, schema_name, fetch=True, **kwargs) -> 'Schema':
|
||||
schema = cls(schema_name)
|
||||
|
||||
# Assign the mandatory properties
|
||||
schema._oid = schema_oid
|
||||
schema._conn = conn
|
||||
|
||||
# Assign the optional properties
|
||||
schema._can_create = kwargs.get('can_create')
|
||||
schema._has_usage = kwargs.get('has_usage')
|
||||
|
||||
# If fetch was requested, do complete refresh
|
||||
if fetch:
|
||||
schema.refresh()
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'Schema':
|
||||
"""
|
||||
Creates an instance of a schema object from the results of a nodes query
|
||||
:param conn: The connection used to execute the nodes query
|
||||
:param kwargs: A row from the nodes query
|
||||
Kwargs:
|
||||
name str: Name of the schema
|
||||
oid int: Object ID of the schema
|
||||
can_create bool: Whether or not the schema can be created by the current user
|
||||
has_usage bool: Whether or not the schema can be used(?)
|
||||
:return:
|
||||
"""
|
||||
schema = cls(conn, kwargs['name'])
|
||||
schema._oid = kwargs['oid']
|
||||
schema._can_create = kwargs['can_create']
|
||||
schema._has_usage = kwargs['has_usage']
|
||||
|
||||
return schema
|
||||
|
||||
def __init__(self, name: str):
|
||||
#
|
||||
|
||||
self._name: str = name
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str):
|
||||
super(Schema, self).__init__(conn, name)
|
||||
|
||||
# Declare the optional parameters
|
||||
self._conn: utils.querying.ConnectionWrapper = None
|
||||
self._oid: Optional[int] = None
|
||||
self._can_create: Optional[bool] = None
|
||||
self._has_usage: Optional[bool] = None
|
||||
|
||||
# Declare the child items
|
||||
self._tables: List[Table] = []
|
||||
self._views: List[View] = []
|
||||
self._tables: node.NodeCollection = node.NodeCollection(
|
||||
lambda: Table.get_nodes_for_parent(self._conn, self._oid)
|
||||
)
|
||||
self._views: node.NodeCollection = node.NodeCollection(
|
||||
lambda: View.get_nodes_for_parent(self._conn, self.oid)
|
||||
)
|
||||
|
||||
# PROPERTIES ###########################################################
|
||||
@property
|
||||
|
@ -69,20 +65,13 @@ class Schema:
|
|||
def has_usage(self) -> Optional[bool]:
|
||||
return self._has_usage
|
||||
|
||||
# -CHILD OBJECTS #######################################################
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
return self._oid
|
||||
|
||||
@property
|
||||
def tables(self) -> List[Table]:
|
||||
def tables(self) -> node.NodeCollection:
|
||||
return self._tables
|
||||
|
||||
@property
|
||||
def views(self) -> List[View]:
|
||||
def views(self) -> node.NodeCollection:
|
||||
return self._views
|
||||
|
||||
# METHODS ##############################################################
|
||||
|
|
|
@ -3,11 +3,15 @@
|
|||
# Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional, Tuple # noqa
|
||||
from typing import Optional, Tuple # noqa
|
||||
|
||||
from psycopg2.extensions import connection
|
||||
|
||||
from pgsmo.objects.database.database import Database
|
||||
from pgsmo.objects.tablespace.tablespace import Tablespace
|
||||
|
||||
from pgsmo.objects.node_object import NodeCollection
|
||||
from pgsmo.objects.role.role import Role
|
||||
from pgsmo.objects.tablespace.tablespace import Tablespace
|
||||
import pgsmo.utils as utils
|
||||
|
||||
|
||||
|
@ -15,14 +19,13 @@ TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
|||
|
||||
|
||||
class Server:
|
||||
def __init__(self, connection, fetch: bool=True):
|
||||
def __init__(self, conn: connection):
|
||||
"""
|
||||
Initializes a server object using the provided connection
|
||||
:param connection: psycopg2 connection
|
||||
:param fetch: Whether or not to fetch all properties of the server and create child objects, defaults to true
|
||||
:param conn: psycopg2 connection
|
||||
"""
|
||||
# Everything we know about the server will be based on the connection
|
||||
self._conn = utils.querying.ConnectionWrapper(connection)
|
||||
self._conn = utils.querying.ConnectionWrapper(conn)
|
||||
|
||||
# Declare the server properties
|
||||
props = self._conn.connection.get_dsn_parameters()
|
||||
|
@ -35,13 +38,9 @@ class Server:
|
|||
self._wal_paused: Optional[bool] = None
|
||||
|
||||
# Declare the child objects
|
||||
self._databases: Optional[List[Database]] = None
|
||||
self._roles: Optional[List[Role]] = None
|
||||
self._tablespaces: Optional[List[Tablespace]] = None
|
||||
|
||||
# Fetch the data for the server
|
||||
if fetch:
|
||||
self.refresh()
|
||||
self._databases: NodeCollection = NodeCollection(lambda: Database.get_nodes_for_parent(self._conn))
|
||||
self._roles: NodeCollection = NodeCollection(lambda: Role.get_nodes_for_parent(self._conn))
|
||||
self._tablespaces: NodeCollection = NodeCollection(lambda: Tablespace.get_nodes_for_parent(self._conn))
|
||||
|
||||
# PROPERTIES ###########################################################
|
||||
|
||||
|
@ -82,37 +81,23 @@ class Server:
|
|||
|
||||
# -CHILD OBJECTS #######################################################
|
||||
@property
|
||||
def databases(self) -> Optional[List[Database]]:
|
||||
def databases(self) -> NodeCollection:
|
||||
"""Databases that belong to the server"""
|
||||
return self._databases
|
||||
|
||||
def roles(self) -> Optional[List[Role]]:
|
||||
@property
|
||||
def roles(self) -> NodeCollection:
|
||||
"""Roles that belong to the server"""
|
||||
return self._roles
|
||||
|
||||
@property
|
||||
def tablespaces(self) -> Optional[List[Tablespace]]:
|
||||
def tablespaces(self) -> NodeCollection:
|
||||
"""Tablespaces defined for the server"""
|
||||
return self._tablespaces
|
||||
|
||||
# METHODS ##############################################################
|
||||
def refresh(self) -> None:
|
||||
"""Refreshes properties of the server and initializes the child items"""
|
||||
self._fetch_recovery_state()
|
||||
self._fetch_databases()
|
||||
self._fetch_roles()
|
||||
self._fetch_tablespaces()
|
||||
|
||||
# IMPLEMENTATION DETAILS ###############################################
|
||||
def _fetch_databases(self) -> None:
|
||||
self._databases = Database.get_databases_for_server(self._conn)
|
||||
|
||||
def _fetch_roles(self) -> None:
|
||||
self._roles = Role.get_roles_for_server(self._conn)
|
||||
|
||||
def _fetch_tablespaces(self) -> None:
|
||||
self._tablespaces = Tablespace.get_tablespaces_for_server(self._conn)
|
||||
|
||||
def _fetch_recovery_state(self) -> None:
|
||||
recovery_check_sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(TEMPLATE_ROOT, 'check_recovery.sql', self._conn.version)
|
||||
|
|
|
@ -3,68 +3,51 @@
|
|||
# Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import pgsmo.objects.column.column as col
|
||||
from pgsmo.objects.column.column import Column
|
||||
import pgsmo.objects.node_object as node
|
||||
import pgsmo.utils as utils
|
||||
|
||||
|
||||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
||||
|
||||
|
||||
class Table:
|
||||
@staticmethod
|
||||
def get_tables_for_schema(conn: utils.querying.ConnectionWrapper, schema_id: int) -> List['Table']:
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(TEMPLATE_ROOT, 'nodes.sql', conn.version),
|
||||
scid=schema_id
|
||||
)
|
||||
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [Table._from_node_query(conn, row['oid'], row['name'], **row) for row in rows]
|
||||
class Table(node.NodeObject):
|
||||
@classmethod
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper, schema_id: int) -> List['Table']:
|
||||
return node.get_nodes(conn, TEMPLATE_ROOT, cls._from_node_query, scid=schema_id)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn, table_oid: int, table_name: str, fetch=True, **kwargs) -> 'Table':
|
||||
table = cls(table_name)
|
||||
|
||||
# Assign the mandatory properties
|
||||
table._oid = table_oid
|
||||
table._conn = conn
|
||||
|
||||
# If fetch was requested, do complete refresh
|
||||
if fetch:
|
||||
table.refresh()
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'Table':
|
||||
"""
|
||||
Creates a table instance from the results of a node query
|
||||
:param conn: The connection used to execute the node query
|
||||
:param kwargs: A row from the node query
|
||||
Kwargs:
|
||||
oid int: Object ID of the table
|
||||
name str: Name of the table
|
||||
:return: A table instance
|
||||
"""
|
||||
table = cls(conn, kwargs['name'])
|
||||
table._oid = kwargs['oid']
|
||||
|
||||
return table
|
||||
|
||||
def __init__(self, name: str):
|
||||
self._name: str = name
|
||||
|
||||
# Declare the optional parameters
|
||||
self._conn: Optional[utils.querying.ConnectionWrapper] = None
|
||||
self._oid: Optional[int] = None
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str):
|
||||
super(Table, self).__init__(conn, name)
|
||||
|
||||
# Declare child items
|
||||
self._columns: Optional[List[col.Column]]
|
||||
self._columns: node.NodeCollection = node.NodeCollection(
|
||||
lambda: Column.get_nodes_for_parent(self._conn, self._oid)
|
||||
)
|
||||
|
||||
# PROPERTIES ###########################################################
|
||||
# -CHILD OBJECTS #######################################################
|
||||
@property
|
||||
def columns(self) -> Optional[List[col.Column]]:
|
||||
def columns(self) -> node.NodeCollection:
|
||||
return self._columns
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
return self._oid
|
||||
|
||||
# METHODS ##############################################################
|
||||
def refresh(self) -> None:
|
||||
self._fetch_columns()
|
||||
|
||||
# IMPLEMENTATION DETAILS ###############################################
|
||||
def _fetch_columns(self) -> None:
|
||||
self._columns = col.Column.get_columns_for_table(self._conn, self._oid)
|
||||
self._columns.reset()
|
||||
|
|
|
@ -5,25 +5,21 @@
|
|||
|
||||
from typing import List, Optional
|
||||
|
||||
from pgsmo.objects.node_object import NodeObject, get_nodes
|
||||
import pgsmo.utils as utils
|
||||
|
||||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'templates')
|
||||
|
||||
|
||||
class Tablespace:
|
||||
class Tablespace(NodeObject):
|
||||
@classmethod
|
||||
def get_tablespaces_for_server(cls, conn: utils.querying.ConnectionWrapper) -> List['Tablespace']:
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper) -> List['Tablespace']:
|
||||
"""
|
||||
Creates a list of tablespaces that belong to the server. Intended to be called by Server class
|
||||
:param conn: Connection to a server to use to lookup the information
|
||||
:return: List of tablespaces for the given server
|
||||
"""
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(TEMPLATE_ROOT, 'nodes.sql', conn.version)
|
||||
)
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [cls._from_node_query(conn, **row) for row in rows]
|
||||
return get_nodes(conn, TEMPLATE_ROOT, cls._from_node_query)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'Tablespace':
|
||||
|
@ -33,36 +29,27 @@ class Tablespace:
|
|||
:param kwargs: Row from a node query for a list of
|
||||
:return: A Tablespace instance
|
||||
"""
|
||||
tablespace = cls()
|
||||
tablespace._conn = conn
|
||||
tablespace = cls(conn, kwargs['name'])
|
||||
|
||||
tablespace._oid = kwargs['oid']
|
||||
tablespace._name = kwargs['name']
|
||||
tablespace._owner = kwargs['owner']
|
||||
|
||||
return tablespace
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes internal state of a Tablespace"""
|
||||
self._conn: Optional[utils.querying.ConnectionWrapper] = None
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str):
|
||||
"""
|
||||
Initializes internal state of a Role object
|
||||
:param conn: Connection that executed the role node query
|
||||
:param name: Name of the role
|
||||
"""
|
||||
super(Tablespace, self).__init__(conn, name)
|
||||
|
||||
# Declare basic properties
|
||||
self._oid: Optional[int] = None
|
||||
self._name: Optional[str] = None
|
||||
self._owner: Optional[int] = None
|
||||
|
||||
# PROPERTIES ###########################################################
|
||||
# -BASIC PROPERTIES ####################################################
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
"""Name of the tablespace"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
"""Object ID of the tablespace"""
|
||||
return self._oid
|
||||
|
||||
@property
|
||||
def owner(self) -> Optional[int]:
|
||||
"""Object ID of the user that owns the tablespace"""
|
||||
|
|
|
@ -4,68 +4,50 @@
|
|||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
import os.path as path
|
||||
from typing import List, Optional
|
||||
from typing import List
|
||||
|
||||
import pgsmo.objects.column.column as col
|
||||
from pgsmo.objects.column.column import Column
|
||||
import pgsmo.objects.node_object as node
|
||||
import pgsmo.utils as utils
|
||||
|
||||
TEMPLATE_ROOT = utils.templating.get_template_root(__file__, 'view_templates')
|
||||
|
||||
|
||||
class View:
|
||||
@staticmethod
|
||||
def get_views_for_schema(conn: utils.querying.ConnectionWrapper, scid: int) -> List['View']:
|
||||
class View(node.NodeObject):
|
||||
@classmethod
|
||||
def get_nodes_for_parent(cls, conn: utils.querying.ConnectionWrapper, scid: int) -> List['View']:
|
||||
type_template_root = path.join(TEMPLATE_ROOT, conn.server_type)
|
||||
sql = utils.templating.render_template(
|
||||
utils.templating.get_template_path(type_template_root, 'nodes.sql', conn.version),
|
||||
scid=scid
|
||||
)
|
||||
|
||||
cols, rows = utils.querying.execute_dict(conn, sql)
|
||||
|
||||
return [View._from_node_query(conn, row['oid'], row['name'], **row) for row in rows]
|
||||
return node.get_nodes(conn, type_template_root, cls._from_node_query, scid=scid)
|
||||
|
||||
@classmethod
|
||||
def _from_node_query(cls, conn, view_oid: int, view_name: str, fetch=True, **kwargs) -> 'View':
|
||||
view = cls(view_name)
|
||||
|
||||
# Assign the optional properties
|
||||
view._conn = conn
|
||||
view._oid = view_oid
|
||||
|
||||
# Fetch the children if requested
|
||||
if fetch:
|
||||
view.refresh()
|
||||
def _from_node_query(cls, conn: utils.querying.ConnectionWrapper, **kwargs) -> 'View':
|
||||
"""
|
||||
Creates a view object from the results of a node query
|
||||
:param conn: Connection used to execute the nodes query
|
||||
:param kwargs: A row from the nodes query
|
||||
Kwargs:
|
||||
name str: Name of the view
|
||||
oid int: Object ID of the view
|
||||
:return: A view instance
|
||||
"""
|
||||
view = cls(conn, kwargs['name'])
|
||||
view._oid = kwargs['oid']
|
||||
|
||||
return view
|
||||
|
||||
def __init__(self, name: str):
|
||||
self._name: str = name
|
||||
|
||||
# Declare optional parameters
|
||||
self._conn: Optional[utils.querying.ConnectionWrapper] = None
|
||||
self._oid: Optional[int] = None
|
||||
def __init__(self, conn: utils.querying.ConnectionWrapper, name: str):
|
||||
super(View, self).__init__(conn, name)
|
||||
|
||||
# Declare child items
|
||||
self._columns: Optional[List[col.Column]]
|
||||
self._columns: node.NodeCollection = node.NodeCollection(
|
||||
lambda: Column.get_nodes_for_parent(self._conn, self.oid)
|
||||
)
|
||||
|
||||
# PROPERTIES ###########################################################
|
||||
@property
|
||||
def columns(self) -> Optional[List[col.Column]]:
|
||||
def columns(self) -> node.NodeCollection:
|
||||
return self._columns
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def oid(self) -> Optional[int]:
|
||||
return self._oid
|
||||
|
||||
# METHODS ##############################################################
|
||||
def refresh(self) -> None:
|
||||
self._fetch_columns()
|
||||
|
||||
# IMPLEMENTATION DETAILS ###############################################
|
||||
def _fetch_columns(self) -> None:
|
||||
self._columns = col.Column.get_columns_for_table(self._conn, self._oid)
|
||||
self._columns.reset()
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
# --------------------------------------------------------------------------------------------
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
# --------------------------------------------------------------------------------------------
|
||||
|
||||
import unittest
|
||||
import unittest.mock as mock
|
||||
|
||||
import pgsmo.objects.node_object as node
|
||||
|
||||
|
||||
class TestNodeCollection(unittest.TestCase):
|
||||
def test_init(self):
|
||||
# Setup: Create a mock generator
|
||||
generator = mock.MagicMock()
|
||||
|
||||
# If: I initialize a node collection
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# Then: The internal properties should be set properly
|
||||
self.assertIs(node_collection._generator, generator)
|
||||
self.assertIsNone(node_collection._items)
|
||||
|
||||
def test_index_bad_type(self):
|
||||
# Setup: Create a mock generator and node collection
|
||||
generator = mock.MagicMock()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# If: I ask for items with an invalid type for the index
|
||||
# Then: I should get an exception
|
||||
with self.assertRaises(TypeError):
|
||||
node_collection[1.2]
|
||||
|
||||
def test_index_no_match_oid(self):
|
||||
# Setup: Create a mock generator and node collection
|
||||
generator, mock_objects = _get_mock_generator()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# If: I get an item that doesn't have a matching oid
|
||||
# Then:
|
||||
# ... I should get an exception
|
||||
with self.assertRaises(NameError):
|
||||
node_collection[789]
|
||||
|
||||
# ... The generator should have been called, tho
|
||||
generator.assert_called_once()
|
||||
self.assertIs(node_collection._items, mock_objects)
|
||||
|
||||
def test_index_no_match_name(self):
|
||||
# Setup: Create a mock generator and node collection
|
||||
generator, mock_objects = _get_mock_generator()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# If: I get an item that doesn't have a matching name
|
||||
# Then:
|
||||
# ... I should get an exception
|
||||
with self.assertRaises(NameError):
|
||||
node_collection['c']
|
||||
|
||||
# ... The generator should have been called, tho
|
||||
generator.assert_called_once()
|
||||
self.assertIs(node_collection._items, mock_objects)
|
||||
|
||||
def test_index_match_oid(self):
|
||||
# Setup: Create a mock generator and node collection
|
||||
generator, mock_objects = _get_mock_generator()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# If: I get an item that has a matching oid
|
||||
output = node_collection[456]
|
||||
|
||||
# Then: The item I have should be the expected item
|
||||
self.assertIs(output, mock_objects[1])
|
||||
|
||||
def test_index_match_name(self):
|
||||
# Setup: Create a mock generator and node collection
|
||||
generator, mock_objects = _get_mock_generator()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# If: I get an item that has a matching oid
|
||||
output = node_collection['b']
|
||||
|
||||
# Then: The item I have should be the expected item
|
||||
self.assertIs(output, mock_objects[1])
|
||||
|
||||
def test_iterator(self):
|
||||
# Setup: Create a mock generator and node collection
|
||||
generator, mock_objects = _get_mock_generator()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
|
||||
# If: I iterate over the items in the collection
|
||||
output = [n for n in node_collection]
|
||||
|
||||
# Then: The list should be equivalent to the list of objects
|
||||
self.assertListEqual(output, mock_objects)
|
||||
|
||||
def test_reset(self):
|
||||
# Setup: Create a mock generator and node collection that has been loaded
|
||||
generator, mock_objects = _get_mock_generator()
|
||||
node_collection = node.NodeCollection(generator)
|
||||
node_collection[123] # Force the collection to load
|
||||
|
||||
# If: I reset the collection
|
||||
node_collection.reset()
|
||||
|
||||
# Then:
|
||||
# ... The item collection should be none
|
||||
self.assertIsNone(node_collection._items)
|
||||
|
||||
|
||||
class TestNodeObject(unittest.TestCase):
|
||||
def test_init(self):
|
||||
# If: I create a node object
|
||||
conn = {}
|
||||
node_obj = node.NodeObject(conn, 'abc')
|
||||
|
||||
# Then: The properties should be assigned as defined
|
||||
self.assertIsNone(node_obj._oid)
|
||||
self.assertIsNone(node_obj.oid)
|
||||
|
||||
self.assertEqual(node_obj._name, 'abc')
|
||||
self.assertEqual(node_obj.name, 'abc')
|
||||
|
||||
self.assertIs(node_obj._conn, conn)
|
||||
|
||||
def test_get_nodes(self):
|
||||
# Setup:
|
||||
# ... Create a mockup of a connection wrapper
|
||||
version = (1, 1, 1)
|
||||
|
||||
class MockConn:
|
||||
def __init__(self):
|
||||
self.version = version
|
||||
|
||||
mock_conn = MockConn()
|
||||
|
||||
# ... Create a mock template renderer
|
||||
mock_render = mock.MagicMock(return_value="SQL")
|
||||
mock_template_path = mock.MagicMock(return_value="path")
|
||||
|
||||
# ... Create a mock query executor
|
||||
mock_objs = [{'name': 'abc', 'oid': 123}, {'name': 'def', 'oid': 456}]
|
||||
mock_executor = mock.MagicMock(return_value=([{}, {}], mock_objs))
|
||||
|
||||
# ... Create a mock generator
|
||||
mock_output = {}
|
||||
mock_generator = mock.MagicMock(return_value=mock_output)
|
||||
|
||||
# ... Do the patching
|
||||
with mock.patch('pgsmo.objects.node_object.templating.render_template', mock_render, create=True):
|
||||
with mock.patch('pgsmo.objects.node_object.templating.get_template_path', mock_template_path, create=True):
|
||||
with mock.patch('pgsmo.objects.node_object.querying.execute_dict', mock_executor, create=True):
|
||||
# If: I ask for a collection of nodes
|
||||
kwargs = {'arg1': 'something'}
|
||||
nodes = node.get_nodes(mock_conn, 'root', mock_generator, **kwargs)
|
||||
|
||||
# Then:
|
||||
# ... The template path should have been called once
|
||||
mock_template_path.assert_called_once_with('root', 'nodes.sql', version)
|
||||
|
||||
# ... The template renderer should have been called once
|
||||
mock_render.assert_called_once_with('path', **kwargs)
|
||||
|
||||
# ... A query should have been executed
|
||||
mock_executor.assert_called_once_with(mock_conn, 'SQL')
|
||||
|
||||
# ... The generator should have been called twice with different object props
|
||||
mock_generator.assert_any_call(mock_conn, **mock_objs[0])
|
||||
mock_generator.assert_any_call(mock_conn, **mock_objs[1])
|
||||
|
||||
# ... The output list of nodes should match what the generator created
|
||||
self.assertIsInstance(nodes, list)
|
||||
self.assertListEqual(nodes, [mock_output, mock_output])
|
||||
|
||||
|
||||
def _get_mock_generator():
|
||||
mock_object1 = node.NodeObject(None, 'a')
|
||||
mock_object1._oid = 123
|
||||
|
||||
mock_object2 = node.NodeObject(None, 'b')
|
||||
mock_object2._oid = 456
|
||||
|
||||
mock_objects = [mock_object1, mock_object2]
|
||||
return mock.MagicMock(return_value=mock_objects), mock_objects
|
Загрузка…
Ссылка в новой задаче