Repository: bikeshedder/tusker
Branch: master
Commit: 7be4175038c5
Files: 8
Total size: 29.3 KB
Directory structure:
gitextract_r5z9r1w1/
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── pyproject.toml
├── tusker/
│ ├── __init__.py
│ └── config.py
└── tusker.toml.example
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.env
tusker.toml
tusker.egg-info/
__pycache__/
================================================
FILE: CHANGELOG.md
================================================
# Change Log
## v0.5.1
* Fix error message for invalid backends
* Fix validation of unique backends
* Fix error when `--(un)safe` or `--(no)privileges` were
not passed as arguments.
* Replace `psycopg2-binary` dependency by `psycopg2`
## v0.5.0
* Added support for glob pattern lists for `schema.filename` and
`migration.filename`. Plain strings are still supported.
* Add support for interpolated environment variables within config files.
* Deprecate `migrations.directory` configuration option.
* Update `tomlkit` to version `0.11`
* Update locked dependency versions
## v0.4.8
* Fix "`TypeError: dict is not a sequence" error when
the schema or migration files contain percent characters (`%`).
## v0.4.7
* Fix "A value is required for bind parameter ..." error caused
by SQL files containing code looking like SQLAlchemy parameters
(`:<params>`).
## v0.4.6 [YANKED]
## v0.4.5
* Add support for `\*\*` in glob pattern
* Improve output of SQL errors
## v0.4.4
* Add default config for `migra` config section
## v0.4.3
* Fix `privileges` configuration option
## v0.4.2
* Add `migra.safe` and `migra.permission` to `tusker.toml`
* Add `--safe` and `--unsafe` arguments
* Add `--without-privileges` argument
* Update `tomlkit` to version `0.10`
* Update locked dependency versions
## v0.4.1
* Do not filter by `.sql` extension when using the `migrations.filename`
setting.
## v0.4.0
* Add `migrations.filename` setting which supports a `glob` pattern
* Fix error messages for invalid configurations
* Increase minimum `python` version to `3.6`
* Update `migra` to version `3.0`
* Update `tomlkit` to version `0.7`
* Update `sqlalchemy` to version `1.4`
* Update `psycopg2` to version `2.9`
## v0.3.4
* Fix quoting of database names
## v0.3.3
* Add support for mixing url with other database settings
## v0.3.2
* Fix transaction handling
## v0.3.1
* Execute files specified by `glob` pattern in sorted order
## v0.3.0
* Add `--version` argument
* Add `glob` pattern support for `schema.filename` setting
## v0.2.3
* Replace f-Strings by .format() calls. This fixes Python 3.5 support.
## v0.2.2
* Add support for `database.schema` config option
## v0.2.1
* Add `--with-privileges` option to `diff` and `check` commands.
## v0.2.0
* Add `from` and `to` argument to `diff` command which makes it possible
to compare a schema file, migration files and an existing database.
* Add `--reverse` option to `diff` command.
* Add `check` command
## v0.1.2
* Fix closing of DB connections
## v0.1.1
* Escape schema and migration SQL before execution
## v0.1.0
* First release
================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
================================================
FILE: README.md
================================================
# Tusker
[](https://github.com/bikeshedder/tusker/blob/master/LICENSE)
[](https://pypi.org/project/tusker)
A PostgreSQL specific migration tool
## Elevator pitch
Do you want to write your database schema directly as SQL
which is understood by PostgreSQL?
Do you want to be able to make changes to this schema and
generate the SQL which is required to migrate between the
old and new schema version?
Tusker does exactly this.
## Installation
```shell
pipx install tusker
```
Now you should be able to run tusker. Give it a try:
```shell
tusker --help
```
## Getting started
Once tusker is installed create a new file called `schema.sql`:
```sql
CREATE TABLE fruit (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
name TEXT NOT NULL UNIQUE
);
```
You probably want to create an empty `migrations` directory, too:
```shell
mkdir migrations
```
Now you should be able to create your first migration:
```
tusker diff
```
The migration is printed to the console and all you need to do is
copy and paste the output into a new file in the migrations directory.
Alternatively you can also pipe the output of `tusker diff` into the
target file:
```
tusker diff > migrations/0001_initial.sql
```
After that check that your `schema.sql` and your `migrations` are in sync:
```
tusker diff
```
This should give you an empty output. This means that there is no difference
between applying the migrations in order and the target schema.
Alternatively you can run the check command:
```
tusker check
```
If you want to change the schema in the future simply change the `schema.sql`
and run `tusker diff` to create the migration for you.
Give it a try and change the `schema.sql`:
```sql
CREATE TABLE fruit (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
name TEXT NOT NULL UNIQUE,
color TEXT NOT NULL DEFAULT ''
);
```
Create a new migration:
```
tusker diff > migrations/0002_fruit_color.sql
```
**Congratulations! You are now using SQL to write your migrations. You are no longer limited by a 3rd party data definition language or an object relational wrapper.**
## Configuration
In order to run tusker you do not need a configuration file. The following
defaults are assumed:
- The file containing your database schema is called `schema.sql`
- The directory containing the migrations is called `migrations`
- Your current user can connect to the database using a unix
domain socket without a password.
You can also create a configuration file called `tusker.toml`. The default
configuration looks like that:
```toml
[schema]
filename = "schema.sql"
[migrations]
filename = "migrations/*.sql"
[database]
#host = ""
#port = 5432
#user = ""
#password = ""
dbname = "my_awesome_db"
#schema = "public"
[migra]
safe = false
privileges = false
```
Instead of the exploded form of `host`, `port`, etc. it
is also possible to pass a connection URL:
```toml
[schema]
filename = "schema.sql"
[migrations]
filename = "migrations/*.sql"
[database]
url = "postgresql:///my_awesome_db_connection"
```
You can also use an environment variable in place of a hard-coded value:
```toml
[database]
url = "${DATABASE_URL}"
```
## How can I use the generated SQL files?
The resulting SQL files can either be applied to the database by hand
or by using one of the many great tools and libraries which support
applying SQL files in order.
Some recommendations are:
- NodeJS: [marv](https://www.npmjs.com/package/marv)
- Rust: [refinery](https://crates.io/crates/refinery)
## How does it work?
Upon startup `tusker` reads all files from the `migrations` directory
and runs them on an empty database. Another empty database is created
and the target schema is created. Then those two schemas are
diffed using the excellent [migra](https://pypi.org/project/migra/)
tool and the output printed to the console.
## Tusker is `unsafe` by default
Unlike `migra` the `tusker` command by default does not throw an
exception when a `drop`-statement is generated. Always check your
generated migrations prior to running them. If you want the same
behavior as migra you can either use the `--safe` argument or set
the `migra.safe` configuration option to `True` in your `tusker.toml`
file.
## FAQ
### Is it possible to split the schema into multiple files?
Yes. This feature has been added in 0.3. You can now use `glob` patterns as
part of the `schema.filename` setting. e.g.:
```toml
[schema]
filename = "schema/*.sql"
```
As of 0.4.5 recursive glob patterns are supported as well:
```toml
[schema]
filename = "schema/**/*.sql"
```
### Is it possible to diff the schema and/or migrations against an existing database?
Yes. This feature has been added in 0.2. You can pass a `from` and `to`
argument to the `tusker diff` command. Check the output of `tusker diff --help` for
more details.
### How can I export initial schema from an existing database?
For exporting the initial schema you can use the native Postgres
[pg_dump](https://www.postgresql.org/docs/current/app-pgdump.html) command with
the `--schema-only` option.
### Tusker printed an error and left the temporary databases behind. How can I remove them?
Run `tusker clean`. This will remove all databases which were created
by previous runs of tusker. Tusker only removes databases which are
marked with a `CREATED BY TUSKER` comment.
### What does the `dbname` setting in `tusker.toml` mean?
The `dbname` setting in `tusker.toml` specifies database name to be used when diffing
against your database. This command will print out the difference between the current
database schema and the target schema:
```shell
tusker diff database
```
Note that this command is meant to be run after you have migrated your database.
Tusker also needs to create temporary databases when diffing against the `schema`
and/or `migrations`. The two databases are called `{dbname}_{timestamp}_schema`
and `{dbname}_{timestamp}_migrations`.
The `dbname` setting overrides the database name in connection `url` (if specified).
If neither a `dbname` nor a `url` is specified it will default to `tusker`. Calling
`tusker diff database` only makes sense if you specify a `dbname` or your application
does indeed use a database called `tusker`.
================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "tusker"
version = "0.5.1"
authors = ["Michael P. Jung <michael.jung@terreon.de>"]
license = "Unlicense"
readme = "README.md"
description = "A PostgreSQL specific migration tool"
repository = "https://github.com/bikeshedder/tusker"
homepage = "https://github.com/bikeshedder/tusker"
classifiers = [
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Development Status :: 4 - Beta",
"Topic :: Database",
"Topic :: Utilities",
]
[tool.poetry.scripts]
tusker = "tusker:main"
[tool.poetry.dependencies]
python = "^3.7"
importlib-metadata = {version = "^1.0", python = "<3.8"}
migra = "^3.0.1621480950"
tomlkit = "^0.11"
sqlalchemy = "^1.4.25"
psycopg2 = "^2.9.5"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
================================================
FILE: tusker/__init__.py
================================================
import argparse
from contextlib import contextmanager, ExitStack
from glob import glob
import sys
import time
import warnings
import migra
import psycopg2
from psycopg2 import sql
import sqlalchemy
from .config import Config
TUSKER_COMMENT = (
'CREATED BY TUSKER - If this table is left behind tusker probably '
'crashed and was not able to clean up after itself. Either try '
'running `tusker clean` or remove this database manually.'
)
try:
import importlib.metadata as importlib_metadata
except ModuleNotFoundError:
import importlib_metadata
try:
__version__ = importlib_metadata.version(__name__)
except:
__version__ = 'unknown'
class ExecuteSqlError(Exception):
pass
def execute_sql_file(cursor, filename):
with open(filename) as fh:
sql = fh.read()
sql = sql.strip()
if not sql:
return
try:
cursor.exec_driver_sql(sql.replace('%', '%%'))
except sqlalchemy.exc.SQLAlchemyError as e:
# https://github.com/sqlalchemy/sqlalchemy/blob/9e7c068d669b209713da62da5748579f92d98129/lib/sqlalchemy/exc.py#L699-L709
# To provide more detail on the underlying error, but without printing the original SQL.
if e.orig:
orig = e.orig
error_text = "(%s.%s) %s" % (orig.__class__.__module__, orig.__class__.__name__, str(orig))
else:
error_text = str(e)
raise ExecuteSqlError('Error executing SQL file {}: {}'.format(filename, error_text))
class Tusker:
def __init__(self, config: Config, verbose=False):
self.config = config
self.verbose = verbose
self.conn = self._connect('template1')
self.conn.autocommit = True
def _connect(self, name):
args = self.config.database.args(dbname='template1')
return psycopg2.connect(**args)
def log(self, text):
if self.verbose:
print(text, file=sys.stderr)
@contextmanager
def createengine(self, dbname=None):
override = {'dbname': dbname} if dbname else {}
engine = sqlalchemy.create_engine(
'postgresql://',
connect_args=self.config.database.args(**override)
)
try:
yield engine
finally:
engine.dispose()
@contextmanager
def createdb(self, suffix):
cursor = self.conn.cursor()
now = int(time.time())
dbname = '{}_{}_{}'.format(
self.config.database.args()['dbname'],
now,
suffix
)
cursor.execute(sql.SQL('CREATE DATABASE {}').format(
sql.Identifier(dbname)
))
cursor.execute(sql.SQL('COMMENT ON DATABASE {} IS {}').format(
sql.Identifier(dbname),
sql.Literal(TUSKER_COMMENT)
))
try:
with self.createengine(dbname) as engine:
yield engine
finally:
cursor.execute(sql.SQL('DROP DATABASE {}').format(
sql.Identifier(dbname)
))
@contextmanager
def mgr_schema(self):
with self.createdb('schema') as schema_engine:
with schema_engine.begin() as schema_cursor:
self.log('Creating original schema...')
for filename in self._get_schema_files():
self.log('- {}'.format(filename))
execute_sql_file(schema_cursor, filename)
yield schema_engine
@contextmanager
def mgr_migrations(self):
with self.createdb('migrations') as migrations_engine:
with migrations_engine.begin() as migrations_cursor:
self.log('Creating migrated schema...')
for filename in self._get_migration_files():
self.log('- {}'.format(filename))
execute_sql_file(migrations_cursor, filename)
yield migrations_engine
@contextmanager
def mgr_database(self):
with self.createengine() as database_engine:
with database_engine.begin() as database_cursor:
self.log('Observing database schema...')
yield database_engine
def mgr(self, name):
return getattr(self, 'mgr_{}'.format(name))()
def diff(self, source, target):
self.log('Creating databases...')
with self.mgr(source) as source, self.mgr(target) as target:
self.log('Diffing...')
migration = migra.Migration(
source,
target,
self.config.database.schema,
)
migration.set_safety(self.config.migra.safe)
migration.add_all_changes(privileges=self.config.migra.privileges)
return migration.sql
def check(self, backends):
with ExitStack() as stack:
managers = [(name, stack.enter_context(self.mgr(name)))
for name in backends]
for i in range(len(managers)-1):
source, target = (managers[i], managers[i+1])
self.log('Diffing {} against {}...'.format(
source[0],
target[0]
))
migration = migra.Migration(
source[1],
target[1],
schema=self.config.database.schema
)
migration.set_safety(self.config.migra.safe)
migration.add_all_changes(privileges=self.config.migra.privileges)
if migration.sql:
return (source[0], target[0])
return None
def clean(self):
cursor = self.conn.cursor()
try:
cursor.execute('''
SELECT db.datname
FROM pg_database db
JOIN pg_shdescription dsc ON dsc.objoid = db.oid
WHERE dsc.description = %s;
''', (TUSKER_COMMENT,))
rows = cursor.fetchall()
for row in rows:
dbname = row[0]
self.log('Dropping {} ...'.format(dbname))
cursor.execute(sql.SQL('DROP DATABASE {}').format(
sql.Identifier(dbname)
))
finally:
cursor.close()
def _get_schema_files(self):
for pattern in self.config.schema.filename:
yield from sorted(glob(pattern, recursive=True))
def _get_migration_files(self):
for pattern in self.config.migrations.filename:
yield from sorted(glob(pattern, recursive=True))
def cmd_diff(args, cfg: Config):
tusker = Tusker(cfg, args.verbose)
source = args.source
target = args.target
if args.reverse:
source, target = target, source
try:
sql = tusker.diff(source, target)
print(sql, end='')
except ExecuteSqlError as e:
print(str(e), file=sys.stderr)
sys.exit(1)
def cmd_check(args, cfg: Config):
backends = args.backends
if 'all' in backends:
backends = ['migrations', 'schema', 'database']
tusker = Tusker(cfg, args.verbose)
try:
diff = tusker.check(backends)
except ExecuteSqlError as e:
print(str(e), file=sys.stderr)
sys.exit(1)
if diff:
print('Schemas differ: {} != {}'.format(diff[0], diff[1]))
print('Run `tusker diff` to see the differences')
sys.exit(1)
else:
print('Schemas are identical')
sys.exit(0)
def cmd_clean(args, cfg: Config):
tusker = Tusker(cfg, args.verbose)
tusker.clean()
BACKEND_CHOICES = ['migrations', 'schema', 'database']
class ValidateBackends(argparse.Action):
def __call__(self, parser, args, values, option_string=None):
if 'all' in values:
values = BACKEND_CHOICES
else:
if len(values) <= 1:
choices = ', '.join(map(repr, BACKEND_CHOICES))
raise argparse.ArgumentError(
self,
(
'at least two backends are required to perform '
'the check (choose from {choices}) or pass \'all\' '
'on its own.'.format(choices=choices)
)
)
backends = set()
for value in values:
if value not in BACKEND_CHOICES:
choices = ', '.join(map(repr, BACKEND_CHOICES + ['all']))
msg = 'invalid choice: {!r} (choose from {})'.format(
value,
choices
)
raise argparse.ArgumentError(self, msg)
if value in backends:
msg = 'duplicate found in backend list: {}'.format(value)
raise argparse.ArgumentError(self, msg)
backends.add(value)
setattr(args, self.dest, values)
def add_migra_args(parser):
g = parser.add_mutually_exclusive_group()
g.add_argument(
'--safe',
help='throw an exception if drop-statements are generated.',
action='store_const',
dest='safe',
const=True,
)
g.add_argument(
'--unsafe',
help='don\'t throw an exception if drop-statements are generated.',
action='store_const',
dest='safe',
const=False,
)
g = parser.add_mutually_exclusive_group()
g.add_argument(
'--with-privileges',
help='output privilege differences (ie. grant/revoke statements).',
action='store_const',
dest='privileges',
const=True,
)
g.add_argument(
'--without-privileges',
help='don\'t output privilege differences.',
action='store_const',
dest='privileges',
const=False,
)
def main():
if not sys.warnoptions:
warnings.simplefilter("default")
parser = argparse.ArgumentParser(
description='Generate a database migration.')
parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__))
parser.add_argument(
'--verbose',
help='enable verbose output',
action='store_true',
default=False)
parser.add_argument(
'--config', '-c',
help='the configuration file. Default: tusker.toml',
default='tusker.toml')
subparsers = parser.add_subparsers(
dest='command',
required=True)
parser_diff = subparsers.add_parser(
'diff',
help='show differences between two schemas',
description='''
This command calculates the difference between two database schemas.
The from- and to-parameter accept one of the following backends:
migrations, schema, database
''')
parser_diff.add_argument(
'source',
metavar='from',
nargs='?',
help='from-backend for the diff operation. Default: migrations',
choices=BACKEND_CHOICES,
default='migrations')
parser_diff.add_argument(
'target',
metavar='to',
nargs='?',
help='to-backend for the diff operation. Default: schema',
choices=BACKEND_CHOICES,
default='schema')
parser_diff.add_argument(
'--reverse', '-r',
help='swaps the "from" and "to" arguments creating a reverse diff',
action='store_true')
parser_diff.add_argument(
'--create-extensions-only',
help='Only output create extension statements, nothing else. ',
action='store_true',
)
add_migra_args(parser_diff)
parser_diff.set_defaults(func=cmd_diff)
parser_check = subparsers.add_parser(
'check',
help='check for differences between schemas',
description='''
This command checks for differences between two or more schemas.
Exit code 0 means that the schemas are all in sync. Otherwise the
exit code 1 is used. This is useful for continuous integration checks.
''')
parser_check.set_defaults(func=cmd_check)
parser_check.add_argument(
'backends',
help=(
'at least two backends are required to diff against each other '
'(choose from {}). You can also pass \'all\' on its own to diff '
'all backends against each other.'
).format(
', '.join(map(repr, BACKEND_CHOICES))
),
metavar='backend',
nargs='*',
default=['migrations', 'schema'],
action=ValidateBackends
)
add_migra_args(parser_check)
parser_clean = subparsers.add_parser(
'clean',
help='clean up left over *_migrations or *_schema tables')
parser_clean.set_defaults(func=cmd_clean)
args = parser.parse_args()
if hasattr(args, 'source') and hasattr(args, 'target') and args.source == args.target:
parser.error('to- and from-backend must not be identical')
cfg = Config(args.config)
if getattr(args, 'safe', None) is not None:
cfg.migra.safe = args.safe
if getattr(args, 'privileges', None) is not None:
cfg.migra.privileges = args.privileges
args.func(args, cfg)
================================================
FILE: tusker/config.py
================================================
import os
import re
from psycopg2.extensions import parse_dsn
from tomlkit.toml_file import TOMLFile
class Config:
def __init__(self, filename=None):
env = os.environ
filename = filename or 'tusker.toml'
toml = TOMLFile(filename)
try:
data = toml.read()
except FileNotFoundError:
data = {}
# time to validate some configuration variables
data.setdefault('database', {'dbname': 'tusker'})
data.setdefault('schema', {'filename': ['schema.sql']})
data.setdefault('migrations', {'filename': ['migrations/*.sql']})
data.setdefault('migra', {'safe': False, 'privileges': False})
self.schema = SchemaConfig(data['schema'])
self.migrations = MigrationsConfig(data['migrations'])
self.database = DatabaseConfig(data['database'])
self.migra = MigraConfig(data['migra'])
def __str__(self):
return 'Config(schema={}, migrations={}, database={}, migra={})'.format(
self.schema,
self.migrations,
self.database,
self.migra
)
def replace_from_env_var(matchobj):
env_variable = matchobj.group(1)
try:
return os.environ[env_variable]
except KeyError:
raise ConfigError.missing_env(env_variable)
class ConfigReader:
def __init__(self, data, path):
self.data = data
self.path = path
def get(self, name, type, required=False, default=None):
if name not in self.data:
if required:
raise ConfigError.missing('{}.{}'.format(self.path, name))
else:
return default
value = self.data[name]
if isinstance(value, str):
# Replace any environment variables
value = re.sub(r"\${([a-zA-Z_][a-zA-Z_0-9]*)}", replace_from_env_var, value)
if not isinstance(value, type):
raise ConfigError.invalid(name, 'Not of type {}'.format(type))
return value
def get_list(self, name, required=False, default=None):
value = self.get(name, (str, list), required, default)
if isinstance(value, str):
value = [value]
else:
if value and not all(isinstance(x, str) for x in value):
raise ConfigError.invalid(name, 'Not a list of strings {}'.format(value))
return value
class SchemaConfig:
def __init__(self, data):
data = ConfigReader(data, 'schema')
self.filename = data.get_list('filename', default=['schema.sql'])
def __str__(self):
return 'SchemaConfig({!r})'.format(self.__dict__)
class MigrationsConfig:
def __init__(self, data):
data = ConfigReader(data, 'migrations')
directory = data.get('directory', str, False)
if directory:
import warnings
warnings.warn(
'The "migrations.directory" configuration option is '
'deprecated and support for this option will be removed '
'in the next version of tusker. Please replace this by '
'the "migrations.filename" option which does support '
'globbing patterns.',
DeprecationWarning,
stacklevel=2
)
filename = data.get_list('filename')
if filename:
raise ConfigError.invalid(
'migrations directory and filename parameters '
'are mutually exclusive',
)
else:
self.filename = ['{}/*.sql'.format(directory)]
else:
self.filename = data.get_list('filename', default=['migrations/*.sql'])
def __str__(self):
return 'MigrationsConfig({!r})'.format(self.__dict__)
class DatabaseConfig:
def __init__(self, data):
data = ConfigReader(data, 'database')
self.url = data.get('url', str)
self.host = data.get('host', str)
self.port = data.get('port', int)
self.dbname = data.get('dbname', str)
self.user = data.get('user', str)
self.password = data.get('password', str)
self.schema = data.get('schema', str)
def __str__(self):
return 'DatabaseConfig({!r})'.format(self.__dict__)
def args(self, **override):
if self.url:
args = parse_dsn(self.url)
else:
args = {}
for k in ['host', 'port', 'dbname', 'user', 'password']:
v = getattr(self, k)
if v is not None:
args[k] = v
if not args['dbname']:
args['dbname'] = 'tusker'
args.update(override)
return args
class MigraConfig:
def __init__(self, data):
data = ConfigReader(data, 'migra')
self.safe = data.get('safe', bool, default=False)
self.privileges = data.get('privileges', bool, default=False)
class ConfigError(RuntimeError):
@classmethod
def missing_env(cls, env_variable):
return cls('Missing environment variable: {}'.format(env_variable))
@classmethod
def missing(cls, name):
return cls('Missing configuration: {}'.format(name))
@classmethod
def invalid(cls, name, reason):
return cls('Invalid configuration: {}, {}'.format(name, reason))
================================================
FILE: tusker.toml.example
================================================
[schema]
filename = "schema.sql"
[migrations]
filename = "migrations/*.sql"
[database]
#host = ""
#port = 5432
#user = ""
#password = ""
dbname = "my_awesome_db"
#schema = "public"
[migra]
safe = false
privileges = false
gitextract_r5z9r1w1/ ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pyproject.toml ├── tusker/ │ ├── __init__.py │ └── config.py └── tusker.toml.example
SYMBOL INDEX (48 symbols across 2 files)
FILE: tusker/__init__.py
class ExecuteSqlError (line 32) | class ExecuteSqlError(Exception):
function execute_sql_file (line 36) | def execute_sql_file(cursor, filename):
class Tusker (line 55) | class Tusker:
method __init__ (line 57) | def __init__(self, config: Config, verbose=False):
method _connect (line 63) | def _connect(self, name):
method log (line 67) | def log(self, text):
method createengine (line 72) | def createengine(self, dbname=None):
method createdb (line 84) | def createdb(self, suffix):
method mgr_schema (line 108) | def mgr_schema(self):
method mgr_migrations (line 118) | def mgr_migrations(self):
method mgr_database (line 128) | def mgr_database(self):
method mgr (line 134) | def mgr(self, name):
method diff (line 137) | def diff(self, source, target):
method check (line 150) | def check(self, backends):
method clean (line 171) | def clean(self):
method _get_schema_files (line 190) | def _get_schema_files(self):
method _get_migration_files (line 195) | def _get_migration_files(self):
function cmd_diff (line 199) | def cmd_diff(args, cfg: Config):
function cmd_check (line 213) | def cmd_check(args, cfg: Config):
function cmd_clean (line 232) | def cmd_clean(args, cfg: Config):
class ValidateBackends (line 240) | class ValidateBackends(argparse.Action):
method __call__ (line 241) | def __call__(self, parser, args, values, option_string=None):
function add_migra_args (line 271) | def add_migra_args(parser):
function main (line 304) | def main():
FILE: tusker/config.py
class Config (line 8) | class Config:
method __init__ (line 10) | def __init__(self, filename=None):
method __str__ (line 28) | def __str__(self):
function replace_from_env_var (line 37) | def replace_from_env_var(matchobj):
class ConfigReader (line 45) | class ConfigReader:
method __init__ (line 47) | def __init__(self, data, path):
method get (line 51) | def get(self, name, type, required=False, default=None):
method get_list (line 67) | def get_list(self, name, required=False, default=None):
class SchemaConfig (line 77) | class SchemaConfig:
method __init__ (line 79) | def __init__(self, data):
method __str__ (line 83) | def __str__(self):
class MigrationsConfig (line 87) | class MigrationsConfig:
method __init__ (line 89) | def __init__(self, data):
method __str__ (line 114) | def __str__(self):
class DatabaseConfig (line 118) | class DatabaseConfig:
method __init__ (line 120) | def __init__(self, data):
method __str__ (line 130) | def __str__(self):
method args (line 133) | def args(self, **override):
class MigraConfig (line 148) | class MigraConfig:
method __init__ (line 149) | def __init__(self, data):
class ConfigError (line 155) | class ConfigError(RuntimeError):
method missing_env (line 158) | def missing_env(cls, env_variable):
method missing (line 162) | def missing(cls, name):
method invalid (line 166) | def invalid(cls, name, reason):
Condensed preview — 8 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (31K chars).
[
{
"path": ".gitignore",
"chars": 47,
"preview": ".env\ntusker.toml\ntusker.egg-info/\n__pycache__/\n"
},
{
"path": "CHANGELOG.md",
"chars": 2624,
"preview": "# Change Log\n\n## v0.5.1\n\n* Fix error message for invalid backends\n* Fix validation of unique backends\n* Fix error when `"
},
{
"path": "LICENSE",
"chars": 1210,
"preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
},
{
"path": "README.md",
"chars": 6445,
"preview": "# Tusker\n\n[. The extraction includes 8 files (29.3 KB), approximately 7.2k tokens, and a symbol index with 48 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.