├── docs ├── _static │ └── .gitkeep ├── mongrations │ ├── api.md │ ├── migrations.md │ ├── getting-started.md │ └── index.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── MANIFEST.in ├── mongrations ├── version.py ├── data │ ├── template.txt │ └── mongrationFile.json ├── __init__.py ├── ClassType.py ├── main.py ├── database.py ├── cli.py ├── cache.py └── connect.py ├── examples ├── mongodb │ ├── .env-example │ ├── mongodb_async.py │ └── mongodb.py ├── mysql │ ├── .env-example │ └── mysql.py ├── postgres │ ├── .env-example │ └── postgres.py └── raw │ └── raw_sql.py ├── requirements.txt ├── .gitignore ├── setup.py ├── mongrationFile.json ├── README.md └── LICENSE /docs/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include data *.txt -------------------------------------------------------------------------------- /mongrations/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.4' 2 | -------------------------------------------------------------------------------- /docs/mongrations/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | coming soon -------------------------------------------------------------------------------- /docs/mongrations/migrations.md: -------------------------------------------------------------------------------- 1 | # Migrations 2 | coming soon -------------------------------------------------------------------------------- /examples/mongodb/.env-example: -------------------------------------------------------------------------------- 1 | MONGO_HOST='localhost' 2 | MONGO_PORT=27017 3 | MONGO_DB='mongrations_test' -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Click 2 | motor 3 | pydotenvs 4 | pymongo 5 | PyMySQL 6 | requests 7 | "psycopg[binary,pool]" 8 | -------------------------------------------------------------------------------- /examples/mysql/.env-example: -------------------------------------------------------------------------------- 1 | MYSQL_HOST='localhost' 2 | MYSQL_USER='root' 3 | MYSQL_PASSWORD='password' 4 | MYSQL_PORT=3306 5 | MYSQL_DB='mongrations_test' -------------------------------------------------------------------------------- /examples/postgres/.env-example: -------------------------------------------------------------------------------- 1 | POSTGRES_HOST='localhost' 2 | POSTGRES_USER='root' 3 | POSTGRES_PASSWORD='password' 4 | POSTGRES_PORT=5432 5 | POSTGRES_DB='mongrations_test' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.egg-info 3 | *.egg 4 | *.eggs 5 | *.pyc 6 | .env 7 | .idea/** 8 | dist/** 9 | build/** 10 | mongrations.egg-info/** 11 | **/__pycache__/** 12 | temp/** 13 | old.txt 14 | docs/_build/** 15 | docs/_templates/** 16 | migrations/** 17 | .mongrations/** 18 | .DS_Store 19 | mongrations/data/cache.json -------------------------------------------------------------------------------- /mongrations/data/template.txt: -------------------------------------------------------------------------------- 1 | from mongrations import Mongrations, Database 2 | 3 | class Mongration(Database): 4 | def __init__(self): 5 | super(Database, self).__init__() 6 | 7 | def up(self): 8 | pass 9 | 10 | def down(self): 11 | pass 12 | 13 | 14 | Mongrations(Mongration) 15 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: mongrations/index.rst 2 | 3 | Guides 4 | ======================================= 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | mongrations/getting-started 10 | mongrations/migrations 11 | mongrations/api 12 | 13 | 14 | 15 | Module Documentation 16 | ================== 17 | .. toctree:: 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /mongrations/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from mongrations.main import Mongrations, MongrationsCli 3 | from mongrations.database import Database 4 | from mongrations.version import __version__ 5 | except ImportError: 6 | from .main import Mongrations, MongrationsCli 7 | from .database import Database 8 | from .version import __version__ 9 | 10 | 11 | __all__ = [Mongrations, MongrationsCli, Database, __version__] -------------------------------------------------------------------------------- /mongrations/ClassType.py: -------------------------------------------------------------------------------- 1 | try: 2 | from pymysql.connections import Connection 3 | from pymongo.database import Database 4 | from psycopg2.extensions import cursor 5 | except ImportError: 6 | cursor = None 7 | Connection = None 8 | Database = None 9 | from os import environ 10 | 11 | db_type = { 12 | 'mongodb': Database, 13 | 'mysql': Connection, 14 | 'postgres': cursor 15 | }.get(environ.get('MONGRATIONS_CLASS_TYPE')) 16 | ClassType = db_type 17 | -------------------------------------------------------------------------------- /examples/mongodb/mongodb_async.py: -------------------------------------------------------------------------------- 1 | from mongrations import Mongrations, Database 2 | from pydotenvs import load_env, load_env_object 3 | 4 | # load_env() # connect via environment variables (default) 5 | config = load_env_object('.env-example') # by default it looks for .env in the current directory 6 | 7 | 8 | class Mongration(Database): 9 | def __init__(self): 10 | super(Database, self).__init__() 11 | 12 | def up(self): 13 | self.db['test_collection'].insert_one({'hello': 'world'}) 14 | 15 | def down(self): 16 | self.db['test_collection'].delete_one({'hello': 'world'}) 17 | 18 | 19 | Mongrations(Mongration, 'async', connection_obj=config) 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /examples/mongodb/mongodb.py: -------------------------------------------------------------------------------- 1 | from mongrations import Mongrations, Database 2 | from pydotenvs import load_env, load_env_object 3 | 4 | load_env('.env-example') # by default it looks for .env in the current directory 5 | # connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ] 6 | 7 | 8 | class Mongration(Database): 9 | def __init__(self): 10 | super(Database, self).__init__() 11 | 12 | def up(self): 13 | self.db['test_collection'].insert_one({'hello': 'world'}) 14 | 15 | def down(self): 16 | self.db['test_collection'].delete_one({'hello': 'world'}) 17 | 18 | # To use connection object (parameter): connection_obj = connection_object 19 | Mongrations(Mongration, 'sync') 20 | -------------------------------------------------------------------------------- /examples/raw/raw_sql.py: -------------------------------------------------------------------------------- 1 | from mongrations import Mongrations, Database 2 | from pydotenvs import load_env, load_env_object 3 | 4 | load_env('.env-example') # by default it looks for .env in the current directory 5 | # connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ] 6 | 7 | 8 | class Mongration(Database): 9 | def __init__(self): 10 | super(Database, self).__init__() 11 | 12 | def up(self): 13 | raw_sql = "ALTER TABLE users ADD gender NVARCHAR" 14 | self.raw(raw_sql) 15 | 16 | def down(self): 17 | self.drop_table('users') 18 | 19 | 20 | # To use connection object (parameter): connection_obj = connection_object 21 | Mongrations(Mongration, db_service='mysql') # raw can be used with all three supported DBs (i.e. MySQL, MongoDB & Postgres) 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/mysql/mysql.py: -------------------------------------------------------------------------------- 1 | from mongrations import Mongrations, Database 2 | from pydotenvs import load_env, load_env_object 3 | 4 | load_env('.env-example') # by default it looks for .env in the current directory 5 | # connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ] 6 | 7 | 8 | class Mongration(Database): 9 | def __init__(self): 10 | super(Database, self).__init__() 11 | 12 | def up(self): 13 | column_info = { 14 | 'id': 'INT NOT NULL AUTO_INCREMENT', 15 | 'firstName': 'VARCHAR(255) NOT NULL', 16 | 'lastName': 'VARCHAR(255) NOT NULL', 17 | 'username': 'VARCHAR(255) NOT NULL', 18 | 'isActive': 'BOOLEAN' 19 | } 20 | self.create_table('users', column_info) 21 | self.add_column('users', 'email') 22 | 23 | def down(self): 24 | self.drop_table('users') 25 | 26 | 27 | # To use connection object (parameter): connection_obj = connection_object 28 | Mongrations(Mongration, db_service='mysql') 29 | -------------------------------------------------------------------------------- /examples/postgres/postgres.py: -------------------------------------------------------------------------------- 1 | from mongrations import Mongrations, Database 2 | from pydotenvs import load_env, load_env_object 3 | 4 | load_env('.env-example') # by default it looks for .env in the current directory 5 | # connection_object = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ] 6 | 7 | 8 | class Mongration(Database): 9 | def __init__(self): 10 | super(Database, self).__init__() 11 | 12 | def up(self): 13 | column_info = { 14 | 'id': 'INT NOT NULL AUTO_INCREMENT', 15 | 'firstName': 'VARCHAR(255) NOT NULL', 16 | 'lastName': 'VARCHAR(255) NOT NULL', 17 | 'username': 'VARCHAR(255) NOT NULL', 18 | 'isActive': 'BOOLEAN' 19 | } 20 | self.create_table('users', column_info) 21 | self.remove_column('users', 'isActive') 22 | 23 | def down(self): 24 | self.drop_table('users') 25 | 26 | 27 | # To use connection object (parameter): connection_obj = connection_object 28 | Mongrations(Mongration, db_service='postgres') 29 | -------------------------------------------------------------------------------- /docs/mongrations/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Make sure you have both [pip](https://pip.pypa.io/en/stable/installing/) and 4 | at least version of Python 3.6 before starting. Mongrations uses new format 5 | strings introduced in 3.6. 6 | 7 | ## 1. Install Mongrations 8 | ```bash 9 | pip3 install mongrations 10 | ``` 11 | ## 2. Create .env with connection parameters 12 | ```bash 13 | MYSQL_HOST='localhost' 14 | MYSQL_USER='root' 15 | MYSQL_PASSWORD='password' 16 | MYSQL_PORT=3306 17 | MYSQL_DB='mongrations_test' 18 | ``` 19 | ## 3. Create a migration file 20 | ```bash 21 | mongrations create create-members-table 22 | ``` 23 | 24 | ## 4. Edit migrations 25 | ```python 26 | from mongrations import Mongrations, Database 27 | from pydotenvs import load_env 28 | 29 | load_env() # this will automatically grab your environment variables 30 | 31 | 32 | class Mongration(Database): 33 | def __init__(self): 34 | super(Database, self).__init__() 35 | 36 | def up(self): 37 | column_info = { 38 | 'id': 'INT NOT NULL AUTO_INCREMENT', 39 | 'firstName': 'VARCHAR(255) NOT NULL', 40 | 'lastName': 'VARCHAR(255) NOT NULL', 41 | 'username': 'VARCHAR(255) NOT NULL', 42 | 'isActive': 'BOOLEAN' 43 | } 44 | self.create_table('users', column_info) 45 | self.add_column('users', 'email') 46 | 47 | def down(self): 48 | self.drop_table('users') 49 | 50 | 51 | Mongrations(Mongration, 'sync', db_service='mysql') 52 | ``` 53 | ## 5. Run migrations 54 | ```bash 55 | mongrations migrate 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /docs/mongrations/index.rst: -------------------------------------------------------------------------------- 1 | Mongrations 2 | ================================= 3 | 4 | Mongrations is a migration tool for Python 3.6+. Mongrations started as a MongoDB migrations tool but has introduced MySQL & Postgres 5 | as compatible servers for the Mongrations tool. Later database adaptions could occur in the future. Mention it to us! 6 | 7 | The goal of the project is to provide a simple way to introduce database management for projects that use Python as a 8 | backend, such as Flask, Django & Sanic. 9 | 10 | 11 | Mongrations is developed `on GitHub `_. Contributions are welcome! 12 | 13 | Simple as simple gets 14 | --------------------------- 15 | 16 | .. code:: python 17 | 18 | from mongrations import Mongrations, Database 19 | from pydotenvs import load_env, load_env_object 20 | 21 | load_env() # connect via environment variables (default) 22 | # config = load_env_object() # connect via dictionary of environment variables [ i.e Mongrations(config) ] 23 | 24 | 25 | class Mongration(Database): 26 | def __init__(self): 27 | super(Database, self).__init__() 28 | 29 | def up(self): 30 | self.db['test_collection'].insert_one({'hello': 'world'}) 31 | 32 | def down(self): 33 | self.db['test_collection'].delete_one({'hello': 'world'}) 34 | 35 | 36 | Mongrations(Mongration, 'sync') 37 | 38 | .. note:: 39 | 40 | Mongrations does not support Python 3.5 or below. There are no plans in the near future to support this, but if an 41 | overwhelming majority of users require it, this could change. -------------------------------------------------------------------------------- /mongrations/data/mongrationFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "mongodb": { 4 | "HOST": "", 5 | "PORT": 27017, 6 | "USER": "", 7 | "PASSWORD": "", 8 | "DB_NAME": "", 9 | "AUTH_SOURCE": "admin" 10 | }, 11 | "mysql": { 12 | "HOST": "", 13 | "PORT": 3306, 14 | "USER": "", 15 | "PASSWORD": "", 16 | "DB_NAME": "" 17 | }, 18 | "postgres": { 19 | "HOST": "", 20 | "PORT": 5432, 21 | "USER": "", 22 | "PASSWORD": "", 23 | "DB_NAME": "" 24 | } 25 | }, 26 | "staging": { 27 | "mongodb": { 28 | "HOST": "", 29 | "PORT": 27017, 30 | "USER": "", 31 | "PASSWORD": "", 32 | "DB_NAME": "", 33 | "AUTH_SOURCE": "admin" 34 | }, 35 | "mysql": { 36 | "HOST": "", 37 | "PORT": 3306, 38 | "USER": "", 39 | "PASSWORD": "", 40 | "DB_NAME": "" 41 | }, 42 | "postgres": { 43 | "HOST": "", 44 | "PORT": 5432, 45 | "USER": "", 46 | "PASSWORD": "", 47 | "DB_NAME": "" 48 | } 49 | }, 50 | "production": { 51 | "mongodb": { 52 | "HOST": "", 53 | "PORT": 27017, 54 | "USER": "", 55 | "PASSWORD": "", 56 | "DB_NAME": "", 57 | "AUTH_SOURCE": "admin" 58 | }, 59 | "mysql": { 60 | "HOST": "", 61 | "PORT": 3306, 62 | "USER": "", 63 | "PASSWORD": "", 64 | "DB_NAME": "" 65 | }, 66 | "postgres": { 67 | "HOST": "", 68 | "PORT": 5432, 69 | "USER": "", 70 | "PASSWORD": "", 71 | "DB_NAME": "" 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os.path as path 2 | from setuptools import setup, find_packages 3 | 4 | try: 5 | from mongrations.version import __version__ 6 | except ImportError: 7 | from mongrations import __version__ 8 | 9 | 10 | with open("README.md", "r") as fh: 11 | long_description = fh.read() 12 | 13 | 14 | 15 | setup( 16 | name="mongrations", 17 | version=__version__, 18 | author="AbleInc - Jaylen Douglas", 19 | author_email="douglas.jaylen@gmail.com", 20 | description="Mongrations; a database independent migration and seeding tool for python.", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | url="https://github.com/ableinc/mongrations", 24 | keywords=['migrations', 'python3', 'automation', 'database', 'json', 'nosql', 'python', 'database tool', 25 | 'automation tool', 'open source', 'mongodb', 'mysql', 'postgres', 'sql'], 26 | packages=find_packages(), 27 | include_package_data=True, 28 | package_data={ 29 | 'mongrations': ['mongrations/data/template.txt', 'mongrations/data/mongrationFile.json'] 30 | }, 31 | data_files=[ 32 | ('/mongrations/data', [path.join('mongrations/data', 'template.txt'), path.join('mongrations/data', 'mongrationFile.json')]) 33 | ], 34 | entry_points=''' 35 | [console_scripts] 36 | mongrations=mongrations.cli:cli 37 | ''', 38 | install_requires=['Click', 'motor', 'pydotenvs', 'pymongo', 'PyMySQL', 'requests', 'psycopg[binary,pool]'], 39 | classifiers=[ 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | "Programming Language :: Python :: 3.8", 43 | "Programming Language :: Python :: 3.9", 44 | "Programming Language :: Python :: 3.10", 45 | "Programming Language :: Python :: 3.11", 46 | "Programming Language :: Python :: 3.12", 47 | "Programming Language :: Python :: 3 :: Only", 48 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 49 | "Operating System :: OS Independent" 50 | ], 51 | ) 52 | -------------------------------------------------------------------------------- /mongrationFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "mongodb": { 4 | "HOST": "localhost", 5 | "PORT": 27017, 6 | "USER": "root", 7 | "PASSWORD": "password", 8 | "DB_NAME": "mongrations", 9 | "AUTH_SOURCE": "admin" 10 | }, 11 | "mysql": { 12 | "HOST": "localhost", 13 | "PORT": 3306, 14 | "USER": "root", 15 | "PASSWORD": "password", 16 | "DB_NAME": "mongrations" 17 | }, 18 | "postgres": { 19 | "HOST": "localhost", 20 | "PORT": 5432, 21 | "USER": "root", 22 | "PASSWORD": "password", 23 | "DB_NAME": "mongrations" 24 | } 25 | }, 26 | "staging": { 27 | "mongodb": { 28 | "HOST": "localhost", 29 | "PORT": 27017, 30 | "USER": "root", 31 | "PASSWORD": "password", 32 | "DB_NAME": "mongrations", 33 | "AUTH_SOURCE": "admin" 34 | }, 35 | "mysql": { 36 | "HOST": "localhost", 37 | "PORT": 3306, 38 | "USER": "root", 39 | "PASSWORD": "password", 40 | "DB_NAME": "mongrations" 41 | }, 42 | "postgres": { 43 | "HOST": "localhost", 44 | "PORT": 5432, 45 | "USER": "root", 46 | "PASSWORD": "password", 47 | "DB_NAME": "mongrations" 48 | } 49 | }, 50 | "production": { 51 | "mongodb": { 52 | "HOST": "localhost", 53 | "PORT": 27017, 54 | "USER": "root", 55 | "PASSWORD": "password", 56 | "DB_NAME": "mongrations", 57 | "AUTH_SOURCE": "admin" 58 | }, 59 | "mysql": { 60 | "HOST": "localhost", 61 | "PORT": 3306, 62 | "USER": "root", 63 | "PASSWORD": "password", 64 | "DB_NAME": "mongrations" 65 | }, 66 | "postgres": { 67 | "HOST": "localhost", 68 | "PORT": 5432, 69 | "USER": "root", 70 | "PASSWORD": "password", 71 | "DB_NAME": "mongrations" 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | 17 | # Ensure that mongrations is present in the path, to allow sphinx-apidoc to 18 | # autogenerate documentation from docstrings 19 | root_directory = os.path.dirname(os.getcwd()) 20 | sys.path.insert(0, root_directory) 21 | 22 | import mongrations 23 | 24 | # -- Project information ----------------------------------------------------- 25 | 26 | source_suffix = ['.rst', '.md'] 27 | 28 | master_doc = 'index' 29 | 30 | project = 'Mongrations' 31 | copyright = '2019, Jaylen Douglas - AbleInc' 32 | author = 'Jaylen Douglas - AbleInc' 33 | 34 | # The full version, including alpha/beta/rc tags 35 | release = mongrations.__version__ 36 | version = mongrations.__version__ 37 | 38 | 39 | # -- General configuration --------------------------------------------------- 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = ['recommonmark'] 45 | 46 | # Add any paths that contain templates here, relative to this directory. 47 | templates_path = ['_templates'] 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 53 | 54 | 55 | # -- Options for HTML output ------------------------------------------------- 56 | 57 | # The theme to use for HTML and HTML Help pages. See the documentation for 58 | # a list of builtin themes. 59 | # 60 | html_theme = 'alabaster' 61 | 62 | # Add any paths that contain custom static files (such as style sheets) here, 63 | # relative to this directory. They are copied after the builtin static files, 64 | # so a file named "default.css" will overwrite the builtin "default.css". 65 | html_static_path = ['_static'] 66 | -------------------------------------------------------------------------------- /mongrations/main.py: -------------------------------------------------------------------------------- 1 | from mongrations.cache import Cache 2 | from os import environ, getcwd, remove 3 | from os.path import basename, join 4 | import subprocess, shlex, io, sys 5 | 6 | 7 | # Command Line Interface Class 8 | class MongrationsCli: 9 | def __init__(self): 10 | self._cache = Cache() 11 | self._cache._set_file_path() 12 | 13 | @staticmethod 14 | def _command_line_interface(migrations: list, state: str): 15 | success = True 16 | if len(migrations) == 0: 17 | print('No migrations to run.') 18 | sys.exit(100) 19 | print(f'{state.upper()}: Running {len(migrations)} migration{"" if len(migrations) <= 1 else "s"}...') 20 | for migration in migrations: 21 | migration_file_path = join(getcwd(), migration) 22 | command = shlex.split(f'python3 {migration_file_path}') 23 | proc = subprocess.Popen(command, stdout=subprocess.PIPE, env=environ.copy()) 24 | for line in io.TextIOWrapper(proc.stdout, encoding='utf8', newline=''): 25 | if line.startswith('Error: '): 26 | print(line) 27 | success = False 28 | else: 29 | print(f'=== {basename(migration)} ===') 30 | print(line) 31 | if success is False: 32 | break 33 | if success: 34 | print('Migrations complete.') 35 | 36 | def down(self, last_migration_only=False, specific_file=None): 37 | specific_file_found = False 38 | environ['MIGRATION_MIGRATE_STATE'] = 'DOWN' 39 | migrations = self._cache.migrations_file_list(last_migration=last_migration_only) 40 | if specific_file is not None: 41 | for migration in migrations: 42 | name = basename(migration).replace('.py', '') 43 | if name == specific_file.replace('.py', ''): 44 | migrations = [migration] 45 | specific_file_found = True 46 | break 47 | if not specific_file_found: 48 | print(f'File not found: {specific_file}') 49 | sys.exit(86) 50 | self._command_line_interface(migrations, 'down') 51 | 52 | def migrate(self): 53 | environ['MIGRATION_MIGRATE_STATE'] = 'UP' 54 | migrations = self._cache.migrations_file_list() 55 | self._command_line_interface(migrations, 'migrate') 56 | 57 | def create(self, directory='migrations', name='no_name_migration'): 58 | self._cache._do_inital_write() 59 | self._cache.new_migration(name, directory) 60 | 61 | def undo(self): 62 | environ['MIGRATION_MIGRATE_STATE'] = 'UNDO' 63 | migration = self._cache.undo_migration() 64 | self._command_line_interface([migration], 'undo') 65 | remove(migration) 66 | self._cache._file_system_check() 67 | 68 | def inspector(self): 69 | self._cache.inspect_cache() 70 | 71 | def create_mongration_file(self): 72 | self._cache.create_migration_file() 73 | 74 | 75 | class Mongrations: 76 | def __init__(self, migration_class, state: str = 'sync', db_service: str = 'mongodb', connection_obj: dict = {}): 77 | self._migration_class = migration_class() 78 | self.state = state 79 | self.connection_object = connection_obj 80 | self.db_service = environ.get('MONGRATIONS_SERVICE_NAME', db_service) 81 | try: 82 | if environ['MIGRATION_MIGRATE_STATE'] == 'UP': 83 | self._up() 84 | elif environ['MIGRATION_MIGRATE_STATE'] == 'DOWN' or environ['MIGRATION_MIGRATE_STATE'] == 'UNDO': 85 | self._down() 86 | except KeyError: 87 | print('Migrations must be run with CLI tool or MongrationsCli class.') 88 | sys.exit(99) 89 | 90 | def _up(self): 91 | self._migration_class._set(self.connection_object, self.db_service, self.state) 92 | self._migration_class.up() 93 | self._migration_class._commit_and_close_connection() 94 | 95 | def _down(self): 96 | self._migration_class._set(self.connection_object, self.db_service, self.state) 97 | self._migration_class.down() 98 | self._migration_class._commit_and_close_connection() 99 | -------------------------------------------------------------------------------- /mongrations/database.py: -------------------------------------------------------------------------------- 1 | try: 2 | from mongrations.connect import Connect 3 | import sys 4 | import psycopg 5 | except ImportError: 6 | from .connect import Connect 7 | 8 | """ 9 | This class is a database tool with predefined functions for executing operations on MySQL or Postgres databases. 10 | This class is not used when writing to MongoDB. If using MongoDB, refer to the PyMongo or Motor documentation. 11 | """ 12 | class Database(Connect): 13 | def __init__(self): 14 | super(Connect, self).__init__() 15 | 16 | def _alert(self, func_name, append=''): 17 | if self._db_service == 'mongodb': 18 | print(f'Error: {func_name}() cannot be used with MongoDB {append if not "" else ""}.') 19 | sys.exit(101) 20 | 21 | def create_database(self, database_name): 22 | self._alert(self.create_database.__name__) 23 | try: 24 | with self.db.cursor() as cursor: 25 | sql = f"CREATE DATABASE {database_name}" 26 | cursor.execute(sql) 27 | except (Exception, psycopg.DatabaseError) as error: 28 | print('Error: ', error) 29 | 30 | def create_table(self, table_name: str, column_info: dict): 31 | self._alert(self.create_table.__name__) 32 | try: 33 | with self.db.cursor() as cursor: 34 | sql = f"CREATE TABLE {table_name} (" 35 | for column_name, column_type in column_info.items(): 36 | sql += f"{column_name} {column_type}," 37 | updated_sql = sql[:-1] 38 | updated_sql += ')' 39 | cursor.execute(updated_sql) 40 | except (Exception, psycopg.DatabaseError) as error: 41 | print('Error: ', error) 42 | 43 | def drop_table(self, table_name: str): 44 | self._alert(self.drop_table.__name__) 45 | try: 46 | with self.db.cursor() as cursor: 47 | sql = f"DROP TABLE {table_name}" 48 | cursor.execute(sql) 49 | except (Exception, psycopg.DatabaseError) as error: 50 | print('Error: ', error) 51 | 52 | def add_column(self, table_name: str, column_name: str, data_type: str = 'VARCHAR(255) NOT NULL'): 53 | self._alert(self.add_column.__name__) 54 | try: 55 | with self.db.cursor() as cursor: 56 | sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {data_type}" 57 | cursor.execute(sql) 58 | except (Exception, psycopg.DatabaseError) as error: 59 | print('Error: ', error) 60 | 61 | def remove_column(self, table_name: str, column_name: str): 62 | self._alert(self.remove_column.__name__) 63 | try: 64 | with self.db.cursor() as cursor: 65 | # Create a new record 66 | sql = f"ALTER TABLE {table_name} DROP COLUMN {column_name}" 67 | cursor.execute(sql) 68 | except (Exception, psycopg.DatabaseError) as error: 69 | print('Error: ', error) 70 | 71 | def delete_from(self, table_name, column_name, query): 72 | self._alert(self.delete_from.__name__, 'or MySQL (Postgres Only)') 73 | try: 74 | with self.db.cursor() as cursor: 75 | sql = f"DELETE FROM {table_name} WHERE {column_name} = {query}" 76 | cursor.execute(sql) 77 | except (Exception, psycopg.DatabaseError) as error: 78 | print('Error: ', error) 79 | 80 | def insert_into(self, table_name, column_info): 81 | self._alert(self.insert_into.__name__) 82 | try: 83 | with self.db.cursor() as cursor: 84 | sql = f'INSERT INTO {table_name} (' 85 | for column_name in column_info.keys(): 86 | sql += f'{column_name},' 87 | s_ql = sql[:-1] 88 | s_ql += ') VALUES (' 89 | for value in column_info.values(): 90 | s_ql += f'{value},' 91 | updated_sql = s_ql[:-1] 92 | updated_sql += ')' 93 | cursor.execute(updated_sql) 94 | except (Exception, psycopg.DatabaseError) as error: 95 | print('Error: ', error) 96 | 97 | def raw(self, raw_sql): 98 | try: 99 | with self.db.cursor() as cursor: 100 | sql = f"{raw_sql}" 101 | cursor.execute(sql) 102 | except (Exception, psycopg.DatabaseError) as error: 103 | print('Error: ', error) 104 | 105 | def _commit_and_close_connection(self): 106 | self.db.commit() 107 | self.db.close() 108 | 109 | -------------------------------------------------------------------------------- /mongrations/cli.py: -------------------------------------------------------------------------------- 1 | import click 2 | import sys 3 | import json 4 | import os 5 | # check python version 6 | try: 7 | sys_version = f"{sys.version_info.major}.{sys.version_info.minor}" 8 | min_required_version = "3.6" 9 | if int(sys_version[sys_version.index('.')+1:]) < int(min_required_version[min_required_version.index('.')+1:]): 10 | click.echo(f"Python version 3.6 or greater is required to run mongrations. Your system version: {sys_version}") 11 | sys.exit(98) 12 | except Exception: 13 | pass 14 | 15 | try: 16 | from mongrations.main import MongrationsCli 17 | from mongrations.version import __version__ 18 | except Exception: 19 | from .main import MongrationsCli 20 | from .version import __version__ 21 | 22 | main = MongrationsCli() 23 | 24 | def add_credentials_to_application_environment(service: str, credentials: dict) -> None: 25 | service_prefix = { 26 | "mongodb": "MONGO_", 27 | "mysql": "MYSQL_", 28 | "postgres": "POSTGRES_" 29 | }.get(service, None) 30 | if service_prefix is None: 31 | click.echo('Invalid service name provided.') 32 | sys.exit(86) 33 | for key in credentials: 34 | os.environ[f'{service_prefix}{key}'] = str(credentials[key]) 35 | os.environ['MONGRATIONS_SERVICE_NAME'] = service 36 | 37 | 38 | def mongration_file_operation(file, env, service): 39 | try: 40 | with open(os.path.join(os.getcwd(), file)) as mf: 41 | mongration_file = json.load(mf) 42 | except FileNotFoundError: 43 | click.echo(f"The migration file, {file}, is not a file or is a directory.") 44 | sys.exit(86) 45 | try: 46 | credentials = mongration_file[env][service] 47 | except KeyError: 48 | click.echo("KeyError: Confirm the service name and env are valid.") 49 | sys.exit(86) 50 | add_credentials_to_application_environment(service, credentials) 51 | 52 | 53 | @click.group() 54 | @click.version_option(version=__version__) 55 | def cli(): 56 | """Mongrations; a database independent migration and seeding tool for python. Compatible with MySQL, PostgreSQL and MongoDB.""" 57 | pass 58 | 59 | 60 | @cli.command() 61 | @click.option("--file", required=False, help="Pass a migration file with database credentials.") 62 | @click.option("--env", default="development", required=False, help="Application environment. This is used with --file.") 63 | @click.option("--service", required=False, help="The database service to use. Options: mongodb, mysql or postgres.") 64 | def migrate(file, env, service): 65 | """Run migrations. The up() method will run on all migration files.""" 66 | if file is not None and service is None: 67 | click.echo("You must provide the service name.") 68 | sys.exit(86) 69 | if file is not None: 70 | mongration_file_operation(file, env, service) 71 | main.migrate() 72 | 73 | 74 | @cli.command() 75 | @click.argument('name', nargs=1) 76 | @click.argument('directory', nargs=-1) 77 | def create(name, directory): 78 | """Create a new migration file. You can specify a name after the create command. You may also specify a directory name 79 | after the name argument.""" 80 | if len(directory) > 0: 81 | directory = directory[0] 82 | else: 83 | directory = 'migrations' 84 | if len(name) == 0: 85 | name = 'no_name_migration' 86 | main.create(directory=directory, name=name) 87 | 88 | 89 | @cli.command() 90 | @click.argument('filename', nargs=1) 91 | @click.option("--file", required=False, help="Pass a migration file with database credentials.") 92 | @click.option("--env", default="development", required=False, help="Application environment. This is used with --file.") 93 | @click.option("--service", required=False, help="The database service to use. Options: mongodb, mysql or postgres.") 94 | def down(filename, file, env, service): 95 | """Run the down() method on a specified migration file""" 96 | if file is not None and service is None: 97 | click.echo("You must provide the service name.") 98 | sys.exit(86) 99 | if file is not None: 100 | mongration_file_operation(file, env, service) 101 | main.down(last_migration_only=False, specific_file=filename) 102 | 103 | 104 | @cli.command() 105 | @click.option("--file", required=False, help="Pass a migration file with database credentials.") 106 | @click.option("--env", default="development", required=False, help="Application environment. This is used with --file.") 107 | @click.option("--service", required=False, help="The database service to use. Options: mongodb, mysql or postgres.") 108 | def undo(file, env, service): 109 | """Undo the last migration. Undo will run the down() method on the last migration 110 | file created and delete the migration file.""" 111 | value = click.prompt("Are you sure you want to undo? (Y/n) ") 112 | if value.lower() == 'y': 113 | if file is not None and service is None: 114 | click.echo("You must provide the service name.") 115 | sys.exit(86) 116 | if file is not None: 117 | mongration_file_operation(file, env, service) 118 | main.undo() 119 | 120 | 121 | @cli.command() 122 | def inspect(): 123 | """Display the config file for mongrations. This will print a list of all migrations, 124 | the last migration file created/ran, etc.""" 125 | main.inspector() 126 | 127 | 128 | @cli.command() 129 | @click.option("--file", required=False, help="Pass a migration file with database credentials.") 130 | @click.option("--env", default="development", required=False, help="Application environment. This is used with --file.") 131 | @click.option("--service", required=False, help="The database service to use. Options: mongodb, mysql or postgres.") 132 | @click.option("--all", default=False, help="Run down() method all migration files. This will not delete the migration files.") 133 | def rollback(file, env, service, all): 134 | """Run the down() method on the last migration file. If --all is True the down() 135 | method will run on all migration files.""" 136 | if file is not None and service is None: 137 | click.echo("You must provide the service name.") 138 | sys.exit(86) 139 | if file is not None: 140 | mongration_file_operation(file, env, service) 141 | if all: 142 | main.down() 143 | else: 144 | main.down(last_migration_only=True) 145 | 146 | @cli.command() 147 | def file(): 148 | """Generate a mongrationFile.json at the root of the project directory.""" 149 | main.create_mongration_file() 150 | 151 | 152 | if __name__ == '__main__': 153 | cli() 154 | -------------------------------------------------------------------------------- /mongrations/cache.py: -------------------------------------------------------------------------------- 1 | # Various Read, Write & Fetch functions for cached data 2 | import os.path as path 3 | from os import remove, makedirs, getcwd 4 | import json 5 | from datetime import datetime 6 | import time, pkg_resources 7 | from pathlib import Path 8 | import sys 9 | 10 | 11 | def get_filepath(): 12 | filepath = Path(path.join(getcwd(), "migrations", ".cache.json")) 13 | if not path.isdir(filepath.parent): 14 | try: 15 | makedirs(filepath.parent) 16 | except FileExistsError: 17 | pass 18 | return filepath 19 | 20 | 21 | class Cache: 22 | def __init__(self, verbose: bool = False): 23 | self._verbose = verbose 24 | self._file_path = None 25 | self._reference_file = pkg_resources.resource_filename('mongrations', 'data/template.txt') 26 | self._mongration_file = pkg_resources.resource_filename('mongrations', 'data/mongrationFile.json') 27 | self.initial = False 28 | 29 | def _set_file_path(self): 30 | self._file_path = get_filepath() 31 | 32 | def _do_inital_write(self): 33 | if not path.isfile(self._file_path): 34 | self.initial = True 35 | self._initial_write() 36 | else: 37 | self.initial = False 38 | self._file_system_check() 39 | 40 | def _get_file_object(self): 41 | if self._file_path is None: 42 | raise FileNotFoundError 43 | with open(self._file_path, 'r', encoding='utf-8') as reader: 44 | return json.load(reader) 45 | 46 | def _write_file_obj(self, data, migration_name=''): 47 | new_data = self._collect_meta_data(data, migration_name) 48 | try: 49 | with open(self._file_path, 'w', encoding='utf-8') as writer: 50 | json_obj = json.dumps(new_data, indent=2, sort_keys=True) 51 | writer.write(json_obj) 52 | except json.decoder.JSONDecodeError: 53 | try: 54 | remove(self._file_path) 55 | except OSError as error: 56 | print(f'{self._file_path} could not be saved. Internal error occurred when creating JSON object. Reason: {error}') 57 | 58 | def _collect_meta_data(self, data, migration_name=''): 59 | new_data = data 60 | if self.initial: 61 | new_data.update({'createdAt': str(datetime.now())}) 62 | 63 | if migration_name != '': 64 | old_entries = new_data['migrations'] 65 | old_entries.append(migration_name) 66 | new_data.update({'migrations': old_entries}) 67 | 68 | if len(new_data['migrations']) >= 1: 69 | new_data.update({'lastMigration': new_data['migrations'][-1]}) 70 | new_data.update({'totalMigrations': len(new_data['migrations'])}) 71 | new_data.update({'updatedAt': str(datetime.now())}) 72 | return new_data 73 | 74 | def _initial_write(self): 75 | data = { 76 | "totalMigrations": 0, 77 | "createdAt": "", 78 | "updatedAt": "", 79 | "lastMigration": "", 80 | "migrations": [] 81 | } 82 | self._write_file_obj(data) 83 | 84 | def _file_system_check(self): 85 | file_obj = self._get_file_object() 86 | updated_migrations_list = file_obj['migrations'] 87 | updated_lastMigration = file_obj['lastMigration'] 88 | for mongration in file_obj['migrations']: 89 | if not path.isfile(mongration): 90 | updated_migrations_list.remove(mongration) 91 | if file_obj['lastMigration'] == mongration: 92 | updated_lastMigration = '' 93 | file_obj['migrations'] = updated_migrations_list 94 | file_obj['lastMigration'] = updated_lastMigration 95 | self._write_file_obj(file_obj) 96 | 97 | def new_migration(self, name: str, directory: str): 98 | if '.py' in name: 99 | name = name.replace('.py', '') 100 | try: 101 | d = path.join(getcwd(), directory) 102 | if not path.isdir(d): 103 | makedirs(d) 104 | except FileExistsError: 105 | print('Warning: Migration name already exists. File will still be created.\n') 106 | timestamp = str(time.time())[:str(time.time()).index('.')] 107 | _name_reference = name 108 | name = name + '_' + timestamp + '.py' 109 | migration_path = path.join(getcwd(), directory + '/' + name) 110 | migration_path_relative = path.join(directory + '/' + name) 111 | if path.isfile(migration_path): 112 | self.new_migration(_name_reference, directory) 113 | with open(self._reference_file, 'r', encoding='utf-8') as reference_file: 114 | with open(migration_path_relative, 'w', encoding='utf-8') as migration_file: 115 | migration_file.write(reference_file.read()) 116 | self._write_file_obj(self._get_file_object(), migration_path_relative) 117 | print(f'Created new migration file: {path.basename(migration_path)}') 118 | 119 | def undo_migration(self, remove_migration: bool = False): 120 | try: 121 | cache = self._get_file_object() 122 | if remove_migration: 123 | cache['migrations'] = cache['migrations'][:-1] 124 | self._write_file_obj(cache) 125 | return cache['lastMigration'] 126 | except FileNotFoundError: 127 | print('Cannot undo last migration. No migrations have been created.') 128 | sys.exit(97) 129 | 130 | def migrations_file_list(self, last_migration=False): 131 | try: 132 | cache = self._get_file_object() 133 | if last_migration: 134 | return [cache['lastMigration']] 135 | return cache['migrations'] 136 | except FileNotFoundError: 137 | print('Cannot do operation. No migrations have been created.') 138 | sys.exit(97) 139 | 140 | def inspect_cache(self): 141 | try: 142 | self._file_system_check() 143 | cache = self._get_file_object() 144 | print(json.dumps(cache, indent=2, sort_keys=False)) 145 | print('File location: ', self._file_path) 146 | except FileNotFoundError: 147 | print('Cannot inspect. No migrations have been created.') 148 | 149 | def create_migration_file(self): 150 | file_path = path.join(getcwd(), path.basename(self._mongration_file)) 151 | if path.isfile(file_path): 152 | print('mongrationFile.json already exists at root.') 153 | sys.exit(94) 154 | with open(file_path, mode='w', encoding='utf-8') as mf: 155 | with open(self._mongration_file, mode='r', encoding='utf-8') as open_mf: 156 | data = json.load(open_mf) 157 | mf.write(json.dumps(data, indent=2)) 158 | -------------------------------------------------------------------------------- /mongrations/connect.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | import sys 3 | try: 4 | from pymongo import MongoClient 5 | import motor.motor_asyncio as motor 6 | import pymysql.cursors 7 | import psycopg 8 | except ImportError: 9 | pass 10 | 11 | 12 | """ 13 | Updated: August 2020 14 | 15 | This class is used to connect to the specified database. This will handle connections to all 16 | three supported database servers. 17 | 18 | As the developer you will not need to handle this directly, as it is used within the Database class. 19 | Refer to documentation for further information. 20 | """ 21 | class Connect: 22 | def __init__(self): 23 | self._connection_object = {} 24 | self._db_service = None 25 | self._service_selection = None 26 | self.db = None 27 | self._state = None 28 | 29 | def _connection(self): 30 | connections = { 31 | 'mongodb': { 32 | 'host': environ.get('MONGO_HOST', None) if self._connection_object.get('MONGO_HOST', None) is None else self._connection_object.get('MONGO_HOST', None), 33 | 'port': environ.get('MONGO_PORT', 27017) if self._connection_object.get('MONGO_PORT', 27017) is None else self._connection_object.get('MONGO_PORT', 27017), 34 | 'user': environ.get('MONGO_USER', None) if self._connection_object.get('MONGO_USER', None) is None else self._connection_object.get('MONGO_USER', None), 35 | 'password': environ.get('MONGO_PASSWORD', None) if self._connection_object.get('MONGO_PASSWORD', None) is None else self._connection_object.get('MONGO_PASSWORD', None), 36 | 'db': environ.get('MONGO_DB_NAME', None) if self._connection_object.get('MONGO_DB_NAME', None) is None else self._connection_object.get('MONGO_DB_NAME', None), 37 | 'authSource': environ.get('MONGO_AUTH_SOURCE', 'admin') if self._connection_object.get('MONGO_AUTH_SOURCE', 'admin') is None else self._connection_object.get('MONGO_AUTH_SOURCE', 'admin') 38 | }, 39 | 'mysql': { 40 | 'host': environ.get('MYSQL_HOST', None) if self._connection_object.get('MYSQL_HOST', None) is None else self._connection_object.get('MYSQL_HOST', None), 41 | 'user': environ.get('MYSQL_USER', None) if self._connection_object.get('MYSQL_USER', None) is None else self._connection_object.get('MYSQL_USER', None), 42 | 'password': environ.get('MYSQL_PASSWORD', None) if self._connection_object.get('MYSQL_PASSWORD', None) is None else self._connection_object.get('MYSQL_PASSWORD', None), 43 | 'port': environ.get('MYSQL_PORT', 3306) if self._connection_object.get('MYSQL_PORT', 3306) is None else self._connection_object.get('MYSQL_PORT', 3306), 44 | 'db': environ.get('MYSQL_DB_NAME', None) if self._connection_object.get('MYSQL_DB_NAME', None) is None else self._connection_object.get('MYSQL_DB_NAME', None) 45 | }, 46 | 'postgres': { 47 | 'host': environ.get('POSTGRES_HOST', None) if self._connection_object.get('POSTGRES_HOST', None) is None else self._connection_object.get('POSTGRES_HOST', None), 48 | 'user': environ.get('POSTGRES_USER', None) if self._connection_object.get('POSTGRES_USER', None) is None else self._connection_object.get('POSTGRES_USER', None), 49 | 'password': environ.get('POSTGRES_PASSWORD', None) if self._connection_object.get('POSTGRES_PASSWORD', None) is None else self._connection_object.get('POSTGRES_PASSWORD', None), 50 | 'port': environ.get('POSTGRES_PORT', 5432) if self._connection_object.get('POSTGRES_PORT', 5432) is None else self._connection_object.get('POSTGRES_PORT', 5432), 51 | 'db': environ.get('POSTGRES_DB_NAME', None) if self._connection_object.get('POSTGRES_DB_NAME', None) is None else self._connection_object.get('POSTGRES_DB_NAME', None) 52 | } 53 | } 54 | try: 55 | conn = connections[self._db_service] 56 | if None in conn.values() or conn == None: 57 | raise ValueError 58 | self._service_selection = conn 59 | except KeyError: 60 | print('Error: The database service {} is not supported.'.format(self._db_service)) 61 | sys.exit(95) 62 | except ValueError: 63 | print('Error: All database connection strings are required.') 64 | sys.exit(95) 65 | return True 66 | 67 | def _set(self, connection_object, db_service, state): 68 | self._connection_object = connection_object if not None else {} 69 | self._db_service = db_service 70 | self._state = state 71 | self._connection() 72 | self._get_db() 73 | 74 | def _get_db(self): 75 | db_option = { 76 | 'mongodb': { 77 | 'sync': self._mongo_sync, 78 | 'async': self._mongo_async 79 | }, 80 | 'mysql': self._mysql, 81 | 'postgres': self._postgres 82 | }.get(self._db_service) 83 | if isinstance(db_option, dict): 84 | self.db = db_option[self._state]() 85 | else: 86 | self.db = db_option() 87 | 88 | def _mongo_async(self): 89 | mongo_url = f'mongodb://{self._service_selection["host"]}:{self._service_selection["port"]}/?connectTimeoutMS=15000' 90 | authSource = self._service_selection.get("authSource", False) 91 | if authSource and authSource != 'None': 92 | mongo_url = mongo_url + f'&authSource={authSource}' 93 | client = motor.AsyncIOMotorClient(mongo_url) 94 | return client[self._service_selection["db"]] 95 | 96 | def _mongo_sync(self): 97 | mongo_url = f'mongodb://{self._service_selection["user"]}:{self._service_selection["password"]}@{self._service_selection["host"]}:{self._service_selection["port"]}/?connectTimeoutMS=15000' 98 | authSource = self._service_selection.get("authSource", False) 99 | if authSource and authSource != 'None': 100 | mongo_url = mongo_url + f'&authSource={authSource}' 101 | client = MongoClient(mongo_url) 102 | return client[self._service_selection["db"]] 103 | 104 | def _mysql(self): 105 | config = self._service_selection 106 | connection = pymysql.connect(host=config['host'], 107 | user=config['user'], 108 | password=config['password'], 109 | db=config['db'], 110 | port=int(config['port']), 111 | charset='utf8mb4', 112 | cursorclass=pymysql.cursors.DictCursor) 113 | return connection 114 | 115 | def _postgres(self): 116 | config = self._service_selection 117 | conn = psycopg.connect(host=config['host'], dbname=config['db'], user=config['user'], password=config['password']) 118 | return conn 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mongrations 2 | 3 | ![alt text](https://img.icons8.com/dusk/64/000000/database.png "Mongrations Logo") 4 | A database independent migration and seeding tool for python. Compatible with MySQL, PostgreSQL and MongoDB. 5 | 6 | ## Required 7 | 8 | - Python version 3.6 or above 9 | - pip version 20.3 or above 10 | 11 | ## Getting Started 12 | 13 | 1 . Generate a migration file 14 | ```bash 15 | mongrations create insert-into-members 16 | ``` 17 | 2 . Contents of the generated migration file (*import and class definition are 18 | autogenerated* - **contents of up() and down() methods are user defined**.) 19 | ```python 20 | from mongrations import Mongrations, Database 21 | 22 | # MongoDB example 23 | class Mongration(Database): 24 | def __init__(self): 25 | super(Database, self).__init__() 26 | 27 | def up(self): 28 | collection = self.db['members'] 29 | data = { 30 | 'accountId': 1, 31 | 'username': 'admin', 32 | 'email': 'admin@able.digital', 33 | 'firstName': 'Site', 34 | 'lastName': 'Owner' 35 | } 36 | collection.insert_one(data) 37 | 38 | def down(self): 39 | collection = self.db['members'] 40 | collection.delete_one({'username': 'admin'}) 41 | 42 | 43 | Mongrations(Mongration) 44 | ``` 45 | 3 . Run migrations 46 | ```bash 47 | mongrations migrate 48 | ``` 49 | 50 | ## Install 51 | 52 | ```bash 53 | pip install --upgrade pip 54 | pip install -U mongrations 55 | 56 | ``` 57 | or install locally 58 | ```bash 59 | git clone https://github.com/ableinc/mongrations.git 60 | cd mongrations 61 | python -m pip install -r requirements.txt 62 | python -m pip install . 63 | ``` 64 | 65 | ## Use 66 | 67 | Mongrations comes with a CLI Tool and an import class for a pythonic migration approach. PyMongo, PyMySQL & Psycopg2 are used under 68 | the hood, so follow PyMongo's, 69 | PyMySQL's, or Psycopg2's documentation 70 | for instructions on how to create your migrations. For the environment variable tool used in this application, follow 71 | this repo (its also installed with this package). 72 | 73 | Refer to Mongrations documentation for more information. 74 | 75 | **CLI** 76 | ```bash 77 | Usage: mongrations [OPTIONS] COMMAND [ARGS]... 78 | 79 | Mongrations; a database migration tool for Python 3.6 and above. 80 | 81 | Options: 82 | --version Show the version and exit. 83 | --help Show this message and exit. 84 | 85 | Commands: 86 | create 87 | down 88 | inspect 89 | migrate 90 | undo 91 | ``` 92 | **CLI Examples** 93 | ```bash 94 | mongrations create [name] # create new migration (w/ name) 95 | mongrations migrate # run migrations 96 | mongrations down # tear down migrations 97 | mongrations undo # undo last migration 98 | ``` 99 | 100 | **Mongrations Class** 101 | ```python 102 | from mongrations import MongrationsCli 103 | 104 | migrations = MongrationsCli() 105 | 106 | migrations.create(directory='migrations', name='file_name') 107 | migrations.migrate() 108 | migrations.down() 109 | migrations.undo() 110 | ``` 111 | Run example migration in examples/ folder 112 | 113 | ## Multi-instance 114 | 115 | If your API uses multiple databases to write and read data, you can provide multiple database connections. This can be achieved by providing a connection object (```connection_obj```) to the ```Mongrations``` class in your migrations file. For a ```connection_obj``` example, please refer to the ```examples/``` folder. You can also do this by prepending the service name to your environment variables. 116 | 117 | Supported service names: 118 | 119 | - ```MONGO_``` 120 | - ```MYSQL_``` 121 | - ```POSTGRES_``` 122 | 123 | Example .env file: 124 | 125 | ```properties 126 | MYSQL_HOST=localhost 127 | MYSQL_USER=root 128 | MYSQL_PASSWORD=password 129 | MYSQL_DB_NAME=myapp 130 | MYSQL_PORT=3306 131 | ``` 132 | 133 | **Note:** ```MONGO_``` service name does **NOT** accept ```MONGO_COLLECTION_NAME```. You will need to provide the collection name in your migration file. The synchronous and asynchronous instances of MongoDB use ```admin``` as the ```authSource``` by default. If you do not want to use ```authSource``` please use ```MONGO_AUTH_SOURCE=None```. 134 | 135 | ## Issues 136 | 137 | Please report all issues to repo. 138 | 139 | You **MUST** have write access to your file system to use this application. 140 | 141 | ## Changelog 142 | 143 | January 2023 - Version 1.1.4: 144 | 145 | - Bugfix: CLI tool will now add service name to environment when using ```--mongrationFile.json``` 146 | 147 | January 2023 - Version 1.1.3: 148 | 149 | - Updated: CLI tool to handle ```--mongrationFile``` for rollback and down command 150 | - psycopg will be downloaded by the library. Installing from source is no longer an option. 151 | 152 | January 2023 - Version 1.1.2: 153 | 154 | - Bugfix: postgres connection library fix 155 | - Bugfix: Database connection would close prematurely 156 | 157 | January 2023 - Version 1.1.1: 158 | - You can now use the ```mongrationFile.json``` file to add database connection variables. You can refer to an example of this file [here](mongrationFile.json) 159 | - You can specify the environment with ```--migrationfile``` (default env is development): 160 | ```bash 161 | mongrations migrate --file mongrationFile.json --env development 162 | ``` 163 | - The CLI tool can generate the ```mongrationFile.json``` file for you. Run this command: 164 | ```bash 165 | mongrations file 166 | ``` 167 | 168 | January 2023 - Version 1.1.0: 169 | - Fixed bug with CLI tool where directory argument wasn't being passed properly to the migrate function. 170 | - The CLI tool has new arguments with better helper descriptions 171 | - The database connection class has been updated to provide more enhances connection strings 172 | - The cache system was rebuilt - The way mongrations caches will change in the future 173 | - ```migrations``` directory will not be created until you create your first migration file 174 | - Updated error codes and error messages. 175 | - In the event your PYTHON_PATH is changed and points to a Python version less than 3.6 the CLI tool will prompt you. 176 | 177 | January 2023 - Version 1.0.4: 178 | - The cache system will now keep the cache file in the ```migrations/``` directory at root 179 | - psycopg[binary,pool] will now be installed during pip installation (pip 20.3 > is required) 180 | - Removed the default ```pydotenvs``` import from the migration file 181 | - Time (in ms) will be appended to file names instead of UUIDs 182 | - The library wil be getting a rewrite and released under another name. This will be the last major release to the library under this name. Note: bug fixes will still be published. 183 | 184 | January 2022 - Version 1.0.4: 185 | - Squashed bugs 186 | - Mongrations can now run on Linux 187 | - Default: stdout error output if error occurs during caching operation 188 | - Removed the psycopg2 install step from setup.py 189 | - Simplified how the database connection strings are initialized 190 | - Inspect will now pretty print JSON structure and provide file system location 191 | - Updated ```examples/``` directory 192 | 193 | August 2020: 194 | - Introduced the official version 1.0.0 of Mongrations! 195 | - Rewrote command line tool; much easier and intuiative 196 | - Extracted classes into their own files; reducing clutter 197 | - Added a raw sql function that allows for much more flexibility 198 | - File name rewrites (if you encounter an upgrade error run the option: --no-cache, with pip) 199 | - psycopg2 is now installed optionally (refer to Notes) 200 | - Super fast writing to the system 201 | - Setup.py has been cleaned up. An occasional bug occured when installing 202 | - Added/Updated examples (refer to Github) 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------