* 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:
Benjamin Russell 2017-07-03 17:23:23 -07:00 коммит произвёл GitHub
Родитель 8dece99b43
Коммит 04168d6f09
11 изменённых файлов: 467 добавлений и 309 удалений

Просмотреть файл

@ -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
tests/pgsmo/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -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