diff --git a/nucypher/characters/chaotic.py b/nucypher/characters/chaotic.py index d89731d45..2e3638f97 100644 --- a/nucypher/characters/chaotic.py +++ b/nucypher/characters/chaotic.py @@ -3,6 +3,7 @@ import os from os.path import dirname, abspath import click +import maya from flask import Flask, render_template, Response from sqlalchemy import create_engine, Column, Integer, String from sqlalchemy.ext.declarative import declarative_base @@ -111,35 +112,41 @@ class Felix(Character): _default_crypto_powerups = [SigningPower] - def __init__(self, db_filepath, rest_host, rest_port, *args, **kwargs): + def __init__(self, + db_filepath: str, + rest_host: str, + rest_port: int, + *args, **kwargs): + + # Character super().__init__(*args, **kwargs) + # + # Felix + # + + # Network self.rest_port = rest_port self.rest_host = rest_host - self.db_filepath = db_filepath self.rest_app = None + + # Database + self.db_filepath = db_filepath self.db = None self.engine = create_engine(f'sqlite://{self.db_filepath}', convert_unicode=True) + # Banner self.log.info(FELIX_BANNER.format(bytes(self.stamp).hex())) - def init_db(self): - db_session = scoped_session(sessionmaker(autocommit=False, - autoflush=False, - bind=self.engine)) - Base = declarative_base() - Base.query = db_session.query_property() - - Base.metadata.create_all(bind=self.engine) - def make_web_app(self): + from flask import request from flask_sqlalchemy import SQLAlchemy # WSGI Service self.rest_app = Flask("faucet", template_folder=TEMPLATES_DIR) # Flask Settings - self.rest_app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite://{self.db_filepath}' + self.rest_app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{self.db_filepath}' self.rest_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False self.rest_app.secret_key = "flask rocks!" # FIXME: NO!!! @@ -152,19 +159,31 @@ class Felix(Character): id = self.db.Column(self.db.Integer, primary_key=True) address = self.db.Column(self.db.String) joined = self.db.Column(self.db.String) + amount_received = self.db.Column(self.db.Integer, default=0) + last_airdrop = self.db.Column(self.db.String, nullable=True) rest_app = self.rest_app - @rest_app.route("/") + @rest_app.route("/", methods=['GET']) def home(): return render_template('felix.html') - @rest_app.route("/register") + @rest_app.route("/register", methods=['POST']) def register(): + try: + recipient = Recipient(address=request.form['address'], joined=str(maya.now())) + self.db.session.add(recipient) + self.db.session.commit() + except Exception as e: + self.log.critical(str(e)) return Response(status=200) return rest_app + def create_tables(self) -> None: + self.db.create_all() + return + def start(self, host: str, port: int, dry_run: bool = False): # Server diff --git a/nucypher/cli/characters/felix.py b/nucypher/cli/characters/felix.py index ad52e3e79..c44ebdc3d 100644 --- a/nucypher/cli/characters/felix.py +++ b/nucypher/cli/characters/felix.py @@ -43,64 +43,77 @@ def felix(click_config, registry_filepath): if action == "init": - """Create a brand-new persistent Ursula""" + """Create a brand-new Felix""" + # Validate "Init" Input if not network: raise click.BadArgumentUsage('--network is required to initialize a new configuration.') + # Acquire Keyring Password if not config_root: # Flag config_root = click_config.config_file # Envvar + new_password = click_config._get_password(confirm=True) - ursula_config = FelixConfiguration.generate(password=click_config._get_password(confirm=True), - config_root=config_root, - rest_host=host, - rest_port=discovery_port, - db_filepath=db_filepath, - domains={network} if network else None, - checksum_public_address=checksum_address, - no_registry=no_registry, - registry_filepath=registry_filepath, - provider_uri=provider_uri, - poa=poa) + new_felix_config = FelixConfiguration.generate(password=new_password, + config_root=config_root, + rest_host=host, + rest_port=discovery_port, + db_filepath=db_filepath, + domains={network} if network else None, + checksum_public_address=checksum_address, + no_registry=no_registry, + registry_filepath=registry_filepath, + provider_uri=provider_uri, + poa=poa) - painting.paint_new_installation_help(new_configuration=ursula_config, + # Paint Help + painting.paint_new_installation_help(new_configuration=new_felix_config, config_root=config_root, config_file=config_file) - return - elif action == 'run': + return # <-- do not remove (conditional flow control) - # Domains -> bytes | or default - domains = [bytes(network, encoding='utf-8')] if network else None + # + # Authentication Configurations + # - # Load Ursula from Configuration File - try: - felix_config = FelixConfiguration.from_configuration_file(filepath=config_file, - domains=domains, - registry_filepath=registry_filepath, - provider_uri=provider_uri, - rest_host=host, - rest_port=port, - db_filepath=db_filepath, - poa=poa) - except FileNotFoundError: - click.secho("No Felix Configuration File Found.") - raise click.Abort + # Domains -> bytes | or default + domains = [bytes(network, encoding='utf-8')] if network else None - # Teacher Ursula + # Load Ursula from Configuration File with overrides + try: + felix_config = FelixConfiguration.from_configuration_file(filepath=config_file, + domains=domains, + registry_filepath=registry_filepath, + provider_uri=provider_uri, + rest_host=host, + rest_port=port, + db_filepath=db_filepath, + poa=poa) + except FileNotFoundError: + click.secho(f"No Felix configuration file found at {config_file}. " + f"Check the filepath or run 'nucypher felix init' to create a new system configuration.") + raise click.Abort + + else: + + # Produce Teacher Ursulas teacher_uris = [teacher_uri] if teacher_uri else list() teacher_nodes = actions.load_seednodes(teacher_uris=teacher_uris, min_stake=min_stake, federated_only=False, network_middleware=click_config.middleware) - # Felix + # Produce Felix click_config.unlock_keyring(character_configuration=felix_config) FELIX = felix_config.produce(domains=network, known_nodes=teacher_nodes) + FELIX.make_web_app() # attach web application, but dont start service - # Start web services - FELIX.make_web_app() + if action == "createdb": # Initialize Database + FELIX.create_tables() + + elif action == 'run': # Start web services FELIX.start(host=host, port=port, dry_run=dry_run) - else: + else: # Error raise click.BadArgumentUsage("No such argument {}".format(action)) diff --git a/nucypher/config/characters.py b/nucypher/config/characters.py index bf459fb0d..d22563974 100644 --- a/nucypher/config/characters.py +++ b/nucypher/config/characters.py @@ -21,6 +21,10 @@ import os from constant_sorrow.constants import ( UNINITIALIZED_CONFIGURATION ) +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session + from nucypher.config.constants import DEFAULT_CONFIG_ROOT from nucypher.config.keyring import NucypherKeyring from nucypher.config.node import NodeConfiguration @@ -133,9 +137,13 @@ class FelixConfiguration(NodeConfiguration): from nucypher.characters.chaotic import Felix def __init__(self, db_filepath: str = None, *args, **kwargs) -> None: - self.db_filepath = db_filepath or self.DEFAULT_DB_FILEPATH + + # Character super().__init__(*args, **kwargs) + # Felix + self.db_filepath = db_filepath or os.path.join(self.config_root, self.DEFAULT_DB_NAME) + # Character _CHARACTER_CLASS = Felix _NAME = _CHARACTER_CLASS.__name__.lower() diff --git a/tests/cli/test_felix.py b/tests/cli/test_felix.py index cc703ec91..4917a286a 100644 --- a/tests/cli/test_felix.py +++ b/tests/cli/test_felix.py @@ -16,14 +16,14 @@ from nucypher.utilities.sandbox.constants import ( @pytest_twisted.inlineCallbacks def test_run_felix(click_runner, federated_ursulas): - args = ('felix', 'init', - '--config-root', MOCK_CUSTOM_INSTALLATION_PATH_2, - '--network', TEMPORARY_DOMAIN, - '--no-registry', - '--provider-uri', TEST_PROVIDER_URI) + init_args = ('felix', 'init', + '--config-root', MOCK_CUSTOM_INSTALLATION_PATH_2, + '--network', TEMPORARY_DOMAIN, + '--no-registry', + '--provider-uri', TEST_PROVIDER_URI) user_input = f'{INSECURE_DEVELOPMENT_PASSWORD}\n{INSECURE_DEVELOPMENT_PASSWORD}' - result = click_runner.invoke(nucypher_cli, args, input=user_input, catch_exceptions=False) + result = click_runner.invoke(nucypher_cli, init_args, input=user_input, catch_exceptions=False) assert result.exit_code == 0 configuration_file_location = os.path.join(MOCK_CUSTOM_INSTALLATION_PATH_2, 'felix.config') @@ -52,7 +52,10 @@ def test_run_felix(click_runner, federated_ursulas): test_client = web_app.test_client() response = test_client.get('/') - assert response == 200 + assert response.status_code == 200 + + response = test_client.post('/register', data={'address': '0xdeadbeef'}) + assert response.status_code == 200 d = threads.deferToThread(run_felix) d.addCallback(request_felix_landing_page) diff --git a/tests/conftest.py b/tests/conftest.py index 75571d0bf..47777b551 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,7 +30,7 @@ globalLogPublisher.removeObserver(logToSentry) # Disable click sentry and file logging NucypherClickConfig.log_to_sentry = False -NucypherClickConfig.log_to_file = False +NucypherClickConfig.log_to_file = True # Crash on server error by default WebEmitter._crash_on_error_default = False