Skip to content

Commit

Permalink
New update; v1.0.3. Refer to README changelog.
Browse files Browse the repository at this point in the history
  • Loading branch information
ableinc committed Jan 26, 2022
1 parent 5e44be8 commit 9e26688
Show file tree
Hide file tree
Showing 14 changed files with 129 additions and 97 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class Mongration(Database):
collection = self.db['members']
collection.delete_one({'username': 'admin'})


# By default this will run the UP operation.
# To run DOWN operation use this function parameter: migrate_state = 'DOWN'
Mongrations(Mongration, 'sync')
```
3 . Run migrations
Expand All @@ -51,11 +52,14 @@ pip install --upgrade mongrations
```
or install locally
```bash
git clone https://github.com/ableinc/mongrations.git
cd mongrations
python -m pip install -r requirements.txt
python setup.py install
```

# Use
Mongrations comes with a CLI Tool as well as a class for a pythonic migration approach. PyMongo, PyMySQL & Psycopg2 are used under
Mongrations comes with a CLI Tool and an import class for a pythonic migration approach. PyMongo, PyMySQL & Psycopg2 are used under
the hood, so follow <a href="https://api.mongodb.com/python/current/tutorial.html#getting-a-collection">PyMongo</a>'s,
<a href="https://github.com/PyMySQL/PyMySQL">PyMySQL</a>'s, or <a href="https://github.com/psycopg/psycopg2">Psycopg2</a>'s documentation
for instructions on how to create your migrations. For the environment variable tool used in this application, follow
Expand Down Expand Up @@ -108,7 +112,17 @@ Please report all issues to repo.
You can install psycopg2 from source via setup.py; python setup.py develop. Follow prompts.
You will need root access to development machine to install this tool.

You **MUST** have write access to your file system in order to use this application.

# Changelog
January 2022:
- Squashed bugs
- Mongrations can now run on Linux
- Default: stdout error output if error occurs during caching operation
- Removed the psycopg2 install step from setup.py
- Simplified how the database connection strings are initialized
- Inspect will now pretty print JSON structure and provide file system location

August 2020:
- Introduced the official version 1.0.0 of Mongrations!
- Rewrote command line tool; much easier and intuiative
Expand Down
4 changes: 2 additions & 2 deletions examples/mongodb/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydotenvs import load_env, load_env_object

load_env('.env-example') # by default it looks for .env in the current directory
# config = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]
# connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]


class Mongration(Database):
Expand All @@ -15,5 +15,5 @@ def up(self):
def down(self):
self.db['test_collection'].delete_one({'hello': 'world'})


# To use connection object (parameter): connection_obj = connection_object
Mongrations(Mongration, 'sync')
3 changes: 2 additions & 1 deletion examples/mysql/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydotenvs import load_env, load_env_object

load_env('.env-example') # by default it looks for .env in the current directory
# config = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]
# connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]


class Mongration(Database):
Expand All @@ -24,4 +24,5 @@ def down(self):
self.drop_table('users')


# To use connection object (parameter): connection_obj = connection_object
Mongrations(Mongration, db_service='mysql')
3 changes: 2 additions & 1 deletion examples/postgres/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydotenvs import load_env, load_env_object

load_env('.env-example') # by default it looks for .env in the current directory
# config = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]
# connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]


class Mongration(Database):
Expand All @@ -24,4 +24,5 @@ def down(self):
self.drop_table('users')


# To use connection object (parameter): connection_obj = connection_object
Mongrations(Mongration, db_service='postgres')
3 changes: 2 additions & 1 deletion examples/raw/raw_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pydotenvs import load_env, load_env_object

load_env('.env-example') # by default it looks for .env in the current directory
# config = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]
# connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ]


class Mongration(Database):
Expand All @@ -17,4 +17,5 @@ def down(self):
self.drop_table('users')


# To use connection object (parameter): connection_obj = connection_object
Mongrations(Mongration, db_service='mysql') # raw can be used with all three supported DBs (i.e. MySQL, MongoDB & Postgres)
51 changes: 51 additions & 0 deletions install_psycopg2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import zipfile, io, os, subprocess, shlex, shutil, sys
from setuptools.command.install import install
from time import sleep
import requests


class InstallWrapper(install):
psycopg2_url = 'https://github.com/psycopg/psycopg2/archive/master.zip'

def run(self):
self.install_postgres()
install.run(self)

def install_postgres(self):
# Install Postgres DB Python Tool
save_dir = os.path.join(str(os.getcwd()) + '/temp/')
print('You will be prompted to install Postgres dependencies. One moment...')
sleep(3)
choice = input('Install psycopg2 from source? (y/n) ')
if choice.lower() == 'y':
path_ = input('path to pg_config (default: current directory) > ')
if path_ == '':
path_ = os.getcwd()
sys.path.append(path_)
try:
# Make Directory and Download Driver
os.makedirs(save_dir)
content = requests.get(self.psycopg2_url).content
file = zipfile.ZipFile(io.BytesIO(content))
file.extractall(save_dir)
print('Installing psycopg2 from source...')
# Install Driver
for command in ['cd temp/psycopg2-master && python3 setup.py build && python3 setup.py install',
'python3 -c "import psycopg2"']:
proc = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE)
for line in io.TextIOWrapper(proc.stdout, encoding='utf-8'):
print(line)
except zipfile.BadZipFile:
pass
except subprocess.CalledProcessError:
print(
f'Fatal error installing psycopg2. Manually install by following '
f'this: https://github.com/psycopg/psycopg2')
finally:
print('Installation complete.')
shutil.rmtree(save_dir, True)


if __name__ == '__main__':
wrapper = InstallWrapper()
wrapper.run()
2 changes: 1 addition & 1 deletion mongrations/ClassType.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from pymongo.database import Database
from pymysql.connections import Connection
try:
from pymongo.database import Database
from psycopg2.extensions import cursor
except ImportError:
cursor = None
Expand Down
34 changes: 17 additions & 17 deletions mongrations/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from datetime import datetime
import uuid, pkg_resources
from pathlib import Path
import sys, getpass


def get_filepath():
import sys, getpass
filepath = {
'darwin': Path(f'/Users/{getpass.getuser()}/.mongrations/cache.json'),
'windows': Path('C:/Users/Programs Files/.mongrations/cache.json')
'win32': Path('C:/Users/Programs Files/.mongrations/cache.json'),
'linux': Path(f'/home/{getpass.getuser()}/.mongrations/cache.json')
}.get(sys.platform)
if not path.isdir(filepath.parent):
try:
Expand Down Expand Up @@ -47,10 +48,8 @@ def _write_file_obj(self, data, migration_name=''):
except json.decoder.JSONDecodeError:
try:
remove(self._file_path)
except OSError:
pass
if self._verbose:
print(f'{self._file_path} could not be saved. Internal error occurred when creating JSON object.')
except OSError as error:
print(f'{self._file_path} could not be saved. Internal error occurred when creating JSON object. Reason: {error}')

def _collect_meta_data(self, data, migration_name=''):
new_data = data
Expand All @@ -63,39 +62,39 @@ def _collect_meta_data(self, data, migration_name=''):
new_data.update({'migrations': old_entries})

if len(new_data['migrations']) >= 1:
new_data.update({'last_migration': new_data['migrations'][-1]})
new_data.update({'total_migrations': len(new_data['migrations'])})
new_data.update({'lastMigration': new_data['migrations'][-1]})
new_data.update({'totalMigrations': len(new_data['migrations'])})
new_data.update({'updatedAt': str(datetime.now())})
return new_data

def _initial_write(self):
data = {
"total_migrations": 0,
"totalMigrations": 0,
"createdAt": "",
"updatedAt": "",
"last_migration": "",
"lastMigration": "",
"migrations": []
}
self._write_file_obj(data)

def _file_system_check(self):
file_obj = self._get_file_object()
updated_migrations_list = file_obj['migrations']
updated_last_migration = file_obj['last_migration']
updated_lastMigration = file_obj['lastMigration']
for mongration in file_obj['migrations']:
if not path.isfile(mongration):
updated_migrations_list.remove(mongration)
if file_obj['last_migration'] == mongration:
updated_last_migration = ''
if file_obj['lastMigration'] == mongration:
updated_lastMigration = ''
file_obj['migrations'] = updated_migrations_list
file_obj['last_migration'] = updated_last_migration
file_obj['lastMigration'] = updated_lastMigration
self._write_file_obj(file_obj)

def new_migration(self, name: str, directory):
try:
makedirs(path.join(getcwd(), directory))
except FileExistsError:
pass
print('Warning: Migration name already exists. File will still be created.\n')
name = str(uuid.uuid4())[:16] + '-' + name + '.py'
migration_path = path.join(getcwd(), directory + '/' + name)
reference_file = open(self._reference_file, 'r', encoding='utf-8')
Expand All @@ -110,7 +109,7 @@ def undo_migration(self, remove_migration: bool = False):
if remove_migration:
cache['migrations'] = cache['migrations'].remove(cache.index(cache['migrations'][-1]))
self._write_file_obj(cache)
return cache['last_migration']
return cache['lastMigration']

def migrations_file_list(self):
cache = self._get_file_object()
Expand All @@ -119,4 +118,5 @@ def migrations_file_list(self):
def inspect_cache(self):
self._file_system_check()
cache = self._get_file_object()
print(cache)
print(json.dumps(cache, indent=2, sort_keys=False))
print('File location: ', self._file_path)
2 changes: 1 addition & 1 deletion mongrations/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import click, sys
import click
try:
from mongrations.main import MongrationsCli
from mongrations.version import __version__
Expand Down
24 changes: 12 additions & 12 deletions mongrations/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,24 @@ def _connection(self):
}
}
try:
for server, configs in connections.items():
if server == self._db_service:
for value in connections[server].values():
if value is None:
raise KeyError
self._service_selection = connections[server]
if self._service_selection is None:
raise KeyError
conn = connections[self._db_service]
if None in conn.values() or conn == None:
raise ValueError
self._service_selection = conn
except KeyError:
print('All database configurations required.')
sys.exit(1)
print('Error: The database service {} is not supported.'.format(self._db_service))
return False
except ValueError:
print('Error: All database connection strings are required.')
return False
return True

def _set(self, connection_object, db_service, state):
self._connection_object = connection_object if not None else {}
self._db_service = db_service
self._state = state
self._connection()
self._get_db()
if self._connection():
self._get_db()

def _get_db(self):
db_option = {
Expand Down
6 changes: 4 additions & 2 deletions mongrations/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
try:
from mongrations.connect import Connect
import sys
import psycopg2
except ImportError:
from .connect import Connect

Expand All @@ -16,7 +18,7 @@ def __init__(self):
def _alert(self, func_name, append=''):
if self._db_service == 'mongo':
print(f'Error: {func_name}() cannot be used with MongoDB {append if not "" else ""}.')
sys.exit()
sys.exit(101)

def create_database(self, database_name):
self._alert(self.create_database.__name__)
Expand Down Expand Up @@ -125,7 +127,7 @@ def insert_into(self, table_name, column_info):
finally:
self.db.close()

def raw(raw_sql):
def raw(self, raw_sql):
try:
with self.db.cursor() as cursor:
# Delete a record
Expand Down
27 changes: 17 additions & 10 deletions mongrations/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,40 @@ def __init__(self):

@staticmethod
def _command_line_interface(migrations: list, state: str):
success = True
if len(migrations) == 0:
print('No migrations to run.')
sys.exit()
sys.exit(100)
print(f'{state.upper()}: Running {len(migrations)} migration{"" if len(migrations) <= 1 else "s"}...')
for migration in migrations:
command = shlex.split(f'python3 {migration}')
print(f'=== {basename(migration)} ===')
proc = subprocess.Popen(command, stdout=subprocess.PIPE, env=environ.copy())
for line in io.TextIOWrapper(proc.stdout, encoding='utf8', newline=''):
print(line)
print('Migrations complete.')
if line.startswith('Error: '):
print(line)
success = False
else:
print(f'=== {basename(migration)} ===')
print(line)
if success is False:
break
if success:
print('Migrations complete.')

def down(self):
environ['MONGRATIONS_MIGRATE_STATE'] = 'DOWN'
environ['MIGRATION_MIGRATE_STATE'] = 'DOWN'
migrations = self._cache.migrations_file_list()
self._command_line_interface(migrations, 'down')

def migrate(self):
environ['MONGRATIONS_MIGRATE_STATE'] = 'UP'
environ['MIGRATION_MIGRATE_STATE'] = 'UP'
migrations = self._cache.migrations_file_list()
self._command_line_interface(migrations, 'migrate')

def create(self, directory='migrations', name='-no-name-migration'):
self._cache.new_migration(name, directory)

def undo(self):
environ['MONGRATIONS_MIGRATE_STATE'] = 'DOWN'
migration = self._cache.undo_migration()
self._command_line_interface([migration], 'undo')

Expand All @@ -52,13 +59,13 @@ def __init__(self, migration_class, state: str = 'sync', db_service: str = 'mong
self.connection_object = connection_obj
self.db_service = db_service
try:
if environ['MONGRATIONS_MIGRATE_STATE'] == 'UP':
if environ['MIGRATION_MIGRATE_STATE'] == 'UP':
self._up()
elif environ['MONGRATIONS_MIGRATE_STATE'] == 'DOWN':
elif environ['MIGRATION_MIGRATE_STATE'] == 'DOWN':
self._down()
except KeyError:
print('Migrations must be run with CLI tool or MongrationsCli class.')
sys.exit()
sys.exit(99)

def _up(self):
self._migration_class._set(self.connection_object, self.db_service, self.state)
Expand Down
2 changes: 1 addition & 1 deletion mongrations/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.2'
__version__ = '1.0.3'

0 comments on commit 9e26688

Please sign in to comment.