-
Notifications
You must be signed in to change notification settings - Fork 0
#53: Create a deployment script #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
048819b
18fa25d
f94271b
ae21d0a
f1917f0
35c00b3
63dd9f3
1f1d3db
dd41865
41a0b27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Deployment script | ||
|
||
This _Python_ script is there to help deploy DB objects at a clear Postgres database or database server. | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Expectations are that thes ource files are placed in a directory structure of following properties | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* database creation script is stored in source root, starts with `00_` and has the extension of `.ddl` (those files are skipped unless a database creation switch is provided to the deployment script) | ||
* other required systemic changes (users, extensions) are stored in files in root, their extension is `.ddl` and are processed alphabetically | ||
* it's recommended these to be written in re-exutable way, of they affet the whole server not just the database | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* objects of schemas are in folders (convention: folder name equals schema name) | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* schema creation SQL is stored in file `_.ddl` | ||
* * DB functions are stored in files with `.sql` extension (conventions: file name equals function name) | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* tables are stored in files with `.ddl` extension (conventions: file name equals table name) | ||
* processing order is: schema -> functions (alphabetically) -> tables (alphabetically) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
#!/usr/bin/env python3 | ||
# -*- coding: utf-8 -*- | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import dataclasses | ||
import argparse | ||
import logging | ||
import os | ||
import psycopg2 | ||
import copy | ||
|
||
|
||
@dataclasses.dataclass | ||
class PostgresDBConn: | ||
"""This dataclass contains all information related to making a connection to Postgres DB.""" | ||
username: str | ||
password: str | ||
host: str | ||
database: str | ||
port: int | ||
|
||
|
||
def parse_args() -> argparse.Namespace: | ||
"""CLI args parsing function.""" | ||
parser = argparse.ArgumentParser( | ||
description="Deploys structures to DB. (script version: 1.0)", | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
) | ||
parser.add_argument( | ||
"-ph", "--host", | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
help="database server host (default: \"localhost\")", | ||
default="localhost", | ||
) | ||
parser.add_argument( | ||
"-p", "--port", | ||
help="database server port (default: \"5432\")", | ||
default="5432", | ||
) | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
parser.add_argument( | ||
"-d", "--dbname", | ||
help="database name to connect to (default: \"ursa_unify_db\")", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"-U", "--username", | ||
help="database user name, should be a high privileged account (default: \"postgres\")", | ||
default="postgres", | ||
) | ||
parser.add_argument( | ||
"-W", "--password", | ||
help="database user password", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"-dir", "--dir", | ||
help="the directory of database source files (default: current directory)", | ||
default=os.getcwd() | ||
) | ||
parser.add_argument( | ||
"--create-db", | ||
action="store_true", | ||
help="creates the target database (runs the scripts in the source root starting with '00_', which is/are expected creating the db", | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
return parser.parse_args() | ||
|
||
|
||
def execute_sql(conn_config: PostgresDBConn, sql: str): | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
conn = psycopg2.connect( | ||
database=conn_config.database, | ||
user=conn_config.username, | ||
password=conn_config.password, | ||
host=conn_config.host, | ||
port=conn_config.port | ||
) | ||
|
||
conn.autocommit = True | ||
cursor = conn.cursor() | ||
|
||
cursor.execute(sql) | ||
|
||
conn.commit() | ||
conn.close() | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def read_file(file_name: str) -> str: | ||
logging.debug(f" - reading file `{file_name}`") | ||
file = open(file_name, "r") | ||
result = file.read() | ||
file.close() | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return result | ||
|
||
|
||
def ensure_trailing_slash(path: str) -> str: | ||
if path.endswith('/'): | ||
return path | ||
else: | ||
return path + '/' | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def process_dir(directory: str, conn_config: PostgresDBConn, create_db: bool): | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logging.info(f"Picking up source files from directory `{directory}`") | ||
public_schema = "public" | ||
root = next(os.walk(directory), (None, [], [])) | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
schemas = list(root[1]) | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
files = list(filter(lambda fn: fn.endswith(".ddl"), root[2])) | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# process root files | ||
database_creation_sqls = [] | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
init_sqls = [] | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for filename in files: | ||
if filename.startswith("00_"): | ||
database_creation_sqls.append(filename) | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else: | ||
init_sqls.append(filename) | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# process schemas | ||
schemas_sqls = [] | ||
if public_schema in schemas: | ||
# public folder has to go first | ||
schemas.remove(public_schema) | ||
schemas_sqls += process_schema(directory, public_schema, False) | ||
|
||
for schema in schemas: | ||
schemas_sqls += process_schema(directory, schema, True) | ||
|
||
# execute the collected Sqls | ||
if (len(database_creation_sqls) > 0) and create_db: | ||
logging.info("Creating database") | ||
db_conn_config = copy.copy(conn_config) | ||
db_conn_config.database = "postgres" | ||
sql = "\n".join(map(read_file, database_creation_sqls)) | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
execute_sql(db_conn_config, sql) | ||
|
||
if len(init_sqls) > 0: | ||
logging.info("Initializing the database") | ||
sql = "\n".join(map(read_file, init_sqls)) | ||
execute_sql(conn_config, sql) | ||
if len(schemas_sqls) > 0: | ||
logging.info("Populating the schemas") | ||
miroslavpojer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sql = "\n".join(schemas_sqls) | ||
execute_sql(conn_config, sql) | ||
|
||
|
||
def process_schema(base_dir: str, schema_name: str, expect_schema_creation: bool) -> list[str]: | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logging.info(f" - schema '{schema_name}'") | ||
schema_dir = ensure_trailing_slash(base_dir + schema_name) | ||
schema_creation = [] | ||
functions = [] | ||
lsulak marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tables = [] | ||
has_schema_creation = False | ||
files = os.listdir(schema_dir) | ||
files.sort() | ||
for input_file in files: | ||
if input_file == "_.ddl": | ||
schema_creation = [read_file(schema_dir + input_file)] | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
has_schema_creation = True | ||
elif input_file.endswith(".sql"): | ||
functions.append(read_file(schema_dir + input_file)) | ||
elif input_file.endswith(".ddl"): | ||
tables.append(read_file(schema_dir + input_file)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. potential optimization could be to only get the file names in this function, and then the loading of their content could be done in the same way as on line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More consitent maybe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
because now it has to be hold in memory from this point until it get's garbage collected which is probably after some point of running the sql statements. If read only in that join function, then the memory requirements are the same but it will be held in memory probably for shorter time. Not a big deal in this scenario I know |
||
|
||
if expect_schema_creation and (not has_schema_creation): | ||
benedeki marked this conversation as resolved.
Show resolved
Hide resolved
|
||
logging.warning(f"No schema creation found on path `{schema_dir}`") | ||
|
||
return schema_creation + functions + tables | ||
|
||
|
||
if __name__ == '__main__': | ||
logging.basicConfig( | ||
level=logging.DEBUG, format="%(asctime)s,%(msecs)03d %(levelname)-8s [%(filename)s:%(lineno)s] %(message)s", | ||
) | ||
|
||
parsed_args = parse_args() | ||
|
||
db_conn_details = PostgresDBConn( | ||
username=parsed_args.username, | ||
password=parsed_args.password, | ||
host=parsed_args.host, | ||
database=parsed_args.dbname, | ||
port=parsed_args.port | ||
) | ||
|
||
process_dir(ensure_trailing_slash(parsed_args.dir), db_conn_details, parsed_args.create_db) |
Uh oh!
There was an error while loading. Please reload this page.