commit 6b044f18dbf18f75dd09ed354b1d60ff42b27c49
Author: nolash <dev@holbrook.no>
Date: Fri, 2 Apr 2021 10:31:50 +0200
Initial commit
Diffstat:
16 files changed, 646 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__
+*.pyc
diff --git a/chainqueue/db/__init__.py b/chainqueue/db/__init__.py
@@ -0,0 +1,57 @@
+# standard imports
+import os
+import logging
+
+# local imports
+from chainqueue.db.models.base import SessionBase
+
+logg = logging.getLogger().getChild(__name__)
+
+
+# an Engine, which the Session will use for connection
+# resources
+
+
+def dsn_from_config(config):
+ """Generate a dsn string from the provided config dict.
+
+ The config dict must include all well-known database connection parameters, and must implement the method "get(key)" to retrieve them. Any missing parameters will be be rendered as the literal string "None"
+
+ :param config: Configuration object
+ :type config: Varies
+ :returns: dsn string
+ :rtype: str
+ """
+ scheme = config.get('DATABASE_ENGINE')
+ if config.get('DATABASE_DRIVER') != None:
+ scheme += '+{}'.format(config.get('DATABASE_DRIVER'))
+
+ dsn = ''
+ dsn_out = ''
+ if config.get('DATABASE_ENGINE') == 'sqlite':
+ dsn = '{}:///{}'.format(
+ scheme,
+ config.get('DATABASE_NAME'),
+ )
+ dsn_out = dsn
+
+ else:
+ dsn = '{}://{}:{}@{}:{}/{}'.format(
+ scheme,
+ config.get('DATABASE_USER'),
+ config.get('DATABASE_PASSWORD'),
+ config.get('DATABASE_HOST'),
+ config.get('DATABASE_PORT'),
+ config.get('DATABASE_NAME'),
+ )
+ dsn_out = '{}://{}:{}@{}:{}/{}'.format(
+ scheme,
+ config.get('DATABASE_USER'),
+ '***',
+ config.get('DATABASE_HOST'),
+ config.get('DATABASE_PORT'),
+ config.get('DATABASE_NAME'),
+ )
+ logg.debug('parsed dsn from config: {}'.format(dsn_out))
+ return dsn
+
diff --git a/chainqueue/db/migrations/default/README b/chainqueue/db/migrations/default/README
@@ -0,0 +1 @@
+Generic single-database configuration.
+\ No newline at end of file
diff --git a/chainqueue/db/migrations/default/alembic.ini b/chainqueue/db/migrations/default/alembic.ini
@@ -0,0 +1,85 @@
+# A generic, single database configuration.
+
+[alembic]
+# path to migration scripts
+script_location = .
+
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# timezone to use when rendering the date
+# within the migration file as well as the filename.
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; this defaults
+# to ./versions. When using multiple version
+# directories, initial revisions must be specified with --version-path
+# version_locations = %(here)s/bar %(here)s/bat ./versions
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+sqlalchemy.url = driver://user:pass@localhost/dbname
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts. See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks=black
+# black.type=console_scripts
+# black.entrypoint=black
+# black.options=-l 79
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/chainqueue/db/migrations/default/env.py b/chainqueue/db/migrations/default/env.py
@@ -0,0 +1,77 @@
+from logging.config import fileConfig
+
+from sqlalchemy import engine_from_config
+from sqlalchemy import pool
+
+from alembic import context
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = None
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(
+ url=url,
+ target_metadata=target_metadata,
+ literal_binds=True,
+ dialect_opts={"paramstyle": "named"},
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+ connectable = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix="sqlalchemy.",
+ poolclass=pool.NullPool,
+ )
+
+ with connectable.connect() as connection:
+ context.configure(
+ connection=connection, target_metadata=target_metadata
+ )
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/chainqueue/db/migrations/default/gmon.out b/chainqueue/db/migrations/default/gmon.out
Binary files differ.
diff --git a/chainqueue/db/migrations/default/script.py.mako b/chainqueue/db/migrations/default/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/chainqueue/db/migrations/default/versions/2215c497248b_transaction_cache.py b/chainqueue/db/migrations/default/versions/2215c497248b_transaction_cache.py
@@ -0,0 +1,38 @@
+"""Transaction cache
+
+Revision ID: 2215c497248b
+Revises: c537a0fd8466
+Create Date: 2021-04-02 10:09:11.923949
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2215c497248b'
+down_revision = 'c537a0fd8466'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table(
+ 'tx_cache',
+ sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('otx_id', sa.Integer, sa.ForeignKey('otx.id'), nullable=True),
+ sa.Column('date_created', sa.DateTime, nullable=False),
+ sa.Column('date_updated', sa.DateTime, nullable=False),
+ sa.Column('source_token_address', sa.String(42), nullable=False),
+ sa.Column('destination_token_address', sa.String(42), nullable=False),
+ sa.Column('sender', sa.String(42), nullable=False),
+ sa.Column('recipient', sa.String(42), nullable=False),
+ sa.Column('from_value', sa.NUMERIC(), nullable=False),
+ sa.Column('to_value', sa.NUMERIC(), nullable=True),
+ sa.Column('block_number', sa.BIGINT(), nullable=True),
+ sa.Column('tx_index', sa.Integer, nullable=True),
+ )
+
+
+def downgrade():
+ op.drop_table('tx_cache')
diff --git a/chainqueue/db/migrations/default/versions/3e43847c0717_otx_state_history.py b/chainqueue/db/migrations/default/versions/3e43847c0717_otx_state_history.py
@@ -0,0 +1,30 @@
+"""Otx state history
+
+Revision ID: 3e43847c0717
+Revises: 2215c497248b
+Create Date: 2021-04-02 10:10:58.656139
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '3e43847c0717'
+down_revision = '2215c497248b'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table(
+ 'otx_state_log',
+ sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('otx_id', sa.Integer, sa.ForeignKey('otx.id'), nullable=False),
+ sa.Column('date', sa.DateTime, nullable=False),
+ sa.Column('status', sa.Integer, nullable=False),
+ )
+
+
+def downgrade():
+ op.drop_table('otx_state_log')
diff --git a/chainqueue/db/migrations/default/versions/c537a0fd8466_outgoing_queue.py b/chainqueue/db/migrations/default/versions/c537a0fd8466_outgoing_queue.py
@@ -0,0 +1,35 @@
+"""Outgoing queue
+
+Revision ID: c537a0fd8466
+Revises:
+Create Date: 2021-04-02 10:04:27.092819
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'c537a0fd8466'
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_table(
+ 'otx',
+ sa.Column('id', sa.Integer, primary_key=True),
+ sa.Column('date_created', sa.DateTime, nullable=False),
+ sa.Column('nonce', sa.Integer, nullable=False),
+ sa.Column('tx_hash', sa.Text, nullable=False),
+ sa.Column('signed_tx', sa.Text, nullable=False),
+ sa.Column('status', sa.Integer, nullable=False, default=0),
+ sa.Column('block', sa.Integer),
+ )
+ op.create_index('idx_otx_tx', 'otx', ['tx_hash'], unique=True)
+
+
+def downgrade():
+ op.drop_index('idx_otx_tx')
+ op.drop_table('otx')
diff --git a/chainqueue/db/models/base.py b/chainqueue/db/models/base.py
@@ -0,0 +1,121 @@
+# stanard imports
+import logging
+
+# third-party imports
+from sqlalchemy import Column, Integer
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.pool import (
+ StaticPool,
+ QueuePool,
+ AssertionPool,
+ )
+
+logg = logging.getLogger()
+
+Model = declarative_base(name='Model')
+
+
+class SessionBase(Model):
+ """The base object for all SQLAlchemy enabled models. All other models must extend this.
+ """
+ __abstract__ = True
+
+ id = Column(Integer, primary_key=True)
+
+ engine = None
+ """Database connection engine of the running aplication"""
+ sessionmaker = None
+ """Factory object responsible for creating sessions from the connection pool"""
+ transactional = True
+ """Whether the database backend supports query transactions. Should be explicitly set by initialization code"""
+ poolable = True
+ """Whether the database backend supports connection pools. Should be explicitly set by initialization code"""
+ procedural = True
+ """Whether the database backend supports stored procedures"""
+ localsessions = {}
+ """Contains dictionary of sessions initiated by db model components"""
+
+
+ @staticmethod
+ def create_session():
+ """Creates a new database session.
+ """
+ return SessionBase.sessionmaker()
+
+
+ @staticmethod
+ def _set_engine(engine):
+ """Sets the database engine static property
+ """
+ SessionBase.engine = engine
+ SessionBase.sessionmaker = sessionmaker(bind=SessionBase.engine)
+
+
+ @staticmethod
+ def connect(dsn, pool_size=16, debug=False):
+ """Create new database connection engine and connect to database backend.
+
+ :param dsn: DSN string defining connection.
+ :type dsn: str
+ """
+ e = None
+ if SessionBase.poolable:
+ poolclass = QueuePool
+ if pool_size > 1:
+ e = create_engine(
+ dsn,
+ max_overflow=pool_size*3,
+ pool_pre_ping=True,
+ pool_size=pool_size,
+ pool_recycle=60,
+ poolclass=poolclass,
+ echo=debug,
+ )
+ else:
+ if debug:
+ poolclass = AssertionPool
+ else:
+ poolclass = StaticPool
+
+ e = create_engine(
+ dsn,
+ poolclass=poolclass,
+ echo=debug,
+ )
+ else:
+ e = create_engine(
+ dsn,
+ echo=debug,
+ )
+
+ SessionBase._set_engine(e)
+
+
+ @staticmethod
+ def disconnect():
+ """Disconnect from database and free resources.
+ """
+ SessionBase.engine.dispose()
+ SessionBase.engine = None
+
+
+ @staticmethod
+ def bind_session(session=None):
+ localsession = session
+ if localsession == None:
+ localsession = SessionBase.create_session()
+ localsession_key = str(id(localsession))
+ logg.debug('creating new session {}'.format(localsession_key))
+ SessionBase.localsessions[localsession_key] = localsession
+ return localsession
+
+
+ @staticmethod
+ def release_session(session=None):
+ session_key = str(id(session))
+ if SessionBase.localsessions.get(session_key) != None:
+ logg.debug('commit and destroy session {}'.format(session_key))
+ session.commit()
+ session.close()
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1,4 @@
+pysha3==1.0.2
+hexathon~=0.0.1a7
+alembic==1.4.2
+SQLAlchemy==1.3.20
diff --git a/setup.cfg b/setup.cfg
@@ -0,0 +1,33 @@
+[metadata]
+name = chainqueue
+version = 0.0.1a1
+description = Generic blockchain transaction queue control
+author = Louis Holbrook
+author_email = dev@holbrook.no
+url = https://gitlab.com/nolash/chainlib
+keywords =
+ cic
+ cryptocurrency
+ ethereum
+ solidarity
+ mutual_credit
+classifiers =
+ Programming Language :: Python :: 3
+ Operating System :: OS Independent
+ Development Status :: 3 - Alpha
+ Environment :: Console
+ Intended Audience :: Developers
+ License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
+ Topic :: Internet
+# Topic :: Blockchain :: EVM
+license = GPL3
+licence_files =
+ LICENSE.txt
+
+[options]
+python_requires = >= 3.6
+packages =
+ chainqueue
+
+#[options.entry_points]
+#console_scripts =
diff --git a/setup.py b/setup.py
@@ -0,0 +1,36 @@
+from setuptools import setup
+import configparser
+import os
+
+
+requirements = []
+f = open('requirements.txt', 'r')
+while True:
+ l = f.readline()
+ if l == '':
+ break
+ requirements.append(l.rstrip())
+f.close()
+
+test_requirements = []
+f = open('test_requirements.txt', 'r')
+while True:
+ l = f.readline()
+ if l == '':
+ break
+ test_requirements.append(l.rstrip())
+f.close()
+
+postgres_requirements = [
+ 'psycopg2==2.8.6',
+ ] + requirements
+sqlite_requirements = [
+ ] + requirements
+setup(
+ install_requires=requirements,
+ tests_require=test_requirements,
+ extras_require={
+ 'postgres': postgres_requirements,
+ 'sqlite': sqlite_requirements,
+ }
+ )
diff --git a/tests/base.py b/tests/base.py
@@ -0,0 +1,83 @@
+# standard imports
+import logging
+import unittest
+import tempfile
+import os
+#import pysqlite
+
+# external imports
+from chainlib.chain import ChainSpec
+import alembic
+import alembic.config
+
+# local imports
+from chainqueue.db import dsn_from_config
+from chainqueue.db.models.base import SessionBase
+
+script_dir = os.path.realpath(os.path.dirname(__file__))
+
+logg = logging.getLogger().getChild(__name__)
+
+
+class TestBase(unittest.TestCase):
+
+ def setUp(self):
+ rootdir = os.path.dirname(os.path.dirname(__file__))
+ dbdir = os.path.join(rootdir, 'chainqueue', 'db')
+ #migrationsdir = os.path.join(dbdir, 'migrations', load_config.get('DATABASE_ENGINE'))
+ #if not os.path.isdir(migrationsdir):
+ migrationsdir = os.path.join(dbdir, 'migrations', 'default')
+ logg.info('using migrations directory {}'.format(migrationsdir))
+
+# db_dir = tempfile.mkdtemp()
+# self.db_path = os.path.join(db_dir, 'test.sqlite')
+# config = {
+# 'DATABASE_ENGINE': 'sqlite',
+# 'DATABASE_DRIVER': 'pysqlite',
+# 'DATABASE_NAME': self.db_path,
+# }
+
+ config = {
+ 'DATABASE_ENGINE': 'sqlite',
+ 'DATABASE_DRIVER': 'pysqlite',
+ 'DATABASE_NAME': 'chainqueue.sqlite',
+ }
+ logg.debug('config {}'.format(config))
+
+ dsn = dsn_from_config(config)
+ SessionBase.poolable = False
+ SessionBase.transactional = False
+ SessionBase.procedural = False
+ SessionBase.connect(dsn, debug=False)
+
+ ac = alembic.config.Config(os.path.join(migrationsdir, 'alembic.ini'))
+ ac.set_main_option('sqlalchemy.url', dsn)
+ ac.set_main_option('script_location', migrationsdir)
+
+ alembic.command.downgrade(ac, 'base')
+ alembic.command.upgrade(ac, 'head')
+
+
+
+ self.session = SessionBase.create_session()
+#
+# f = open(os.path.join(script_dir, '..', 'sql', 'sqlite', '1.sql'), 'r')
+# sql = f.read()
+# f.close()
+#
+# conn = SessionBase.engine.connect()
+# conn.execute(sql)
+#
+# f = open(os.path.join(script_dir, '..', 'sql', 'sqlite', '2.sql'), 'r')
+# sql = f.read()
+# f.close()
+#
+# conn = SessionBase.engine.connect()
+# conn.execute(sql)
+#
+ self.chain_spec = ChainSpec('evm', 'foo', 42, 'bar')
+
+
+ def tearDown(self):
+ self.session.commit()
+ self.session.close()
diff --git a/tests/test_basic.py b/tests/test_basic.py
@@ -0,0 +1,19 @@
+# standard imports
+import logging
+import unittest
+
+# test imports
+from tests.base import TestBase
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+class TestBasic(TestBase):
+
+ def test_hello(self):
+ logg.debug('foo')
+ pass
+
+
+if __name__ == '__main__':
+ unittest.main()