Exemplo n.º 1
0
def main() -> None:
    """
    Command-line entry point.
    """
    parser = argparse.ArgumentParser(
        description="Tool to initialize the database used by CRATE's "
        "implementation of an NLPRP server.")
    parser.add_argument(
        "config_uri",
        type=str,
        help="Config file to read (e.g. 'development.ini'); URL of database "
        "is found here.")
    args = parser.parse_args()
    main_only_quicksetup_rootlogger()

    config_file = args.config_uri
    log.debug(f"Settings file: {config_file}")
    settings = get_appsettings(config_file)
    engine = engine_from_config(settings,
                                NlpServerConfigKeys._SQLALCHEMY_PREFIX)
    sqla_url = get_safe_url_from_engine(engine)
    log.info(f"Using database {sqla_url!r}")
    dbsession.configure(bind=engine)
    log.info("Creating database structure...")
    Base.metadata.create_all(engine)
    log.info("... done.")
Exemplo n.º 2
0
def export_whole_database(req: "CamcopsRequest",
                          recipient: ExportRecipient,
                          via_index: bool = True) -> None:
    """
    Exports to a database.
    
    Holds a recipient-specific file lock in the process.
    
    Args:
        req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        recipient: an :class:`camcops_server.cc_modules.cc_exportmodels.ExportRecipient`
        via_index: use the task index (faster)?
    """  # noqa
    cfg = req.config
    lockfilename = cfg.get_export_lockfilename_db(
        recipient_name=recipient.recipient_name)
    try:
        with lockfile.FileLock(lockfilename, timeout=0):  # doesn't wait
            collection = get_collection_for_export(req,
                                                   recipient,
                                                   via_index=via_index)
            dst_engine = create_engine(recipient.db_url,
                                       echo=recipient.db_echo)
            log.info("Exporting to database: {}",
                     get_safe_url_from_engine(dst_engine))
            dst_session = sessionmaker(bind=dst_engine)()  # type: SqlASession
            task_generator = gen_tasks_having_exportedtasks(collection)
            export_options = TaskExportOptions(
                include_blobs=recipient.db_include_blobs,
                # *** todo: other options, specifically DB_PATIENT_ID_PER_ROW
            )
            copy_tasks_and_summaries(
                tasks=task_generator,
                dst_engine=dst_engine,
                dst_session=dst_session,
                export_options=export_options,
                req=req,
            )
            dst_session.commit()
    except lockfile.AlreadyLocked:
        log.warning(
            "Export logfile {!r} already locked by another process; "
            "aborting", lockfilename)
Exemplo n.º 3
0
def make_wsgi_app(global_config: Dict[Any, Any], **settings) -> Router:
    # Logging
    main_only_quicksetup_rootlogger()
    logging.getLogger('sqlalchemy').setLevel(logging.WARNING)

    # log.debug(f"global_config: {global_config!r}")
    # log.debug(f"settings: {settings!r}")

    # Database
    engine = engine_from_config(settings,
                                NlpServerConfigKeys._SQLALCHEMY_PREFIX,
                                **SQLALCHEMY_COMMON_OPTIONS)
    # ... add to config - pool_recycle is set to create new sessions every 7h
    sqla_url = get_safe_url_from_engine(engine)
    log.info(f"Using database {sqla_url!r}")
    dbsession.configure(bind=engine)
    Base.metadata.bind = engine

    # Pyramid
    config = Configurator(settings=settings)

    # Security policies
    authn_policy = AuthTktAuthenticationPolicy(
        settings[NlpServerConfigKeys.NLP_WEBSERVER_SECRET],
        secure=True,  # only allow requests over HTTPS
        hashalg='sha512')
    authz_policy = ACLAuthorizationPolicy()
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

    # Compression
    config.add_tween("cardinal_pythonlib.pyramid.compression.CompressionTweenFactory")  # noqa

    # Routes
    config.add_route('index', '/')
    config.scan('.views')

    # Create WSGI app
    return config.make_wsgi_app()
Exemplo n.º 4
0
def create_table_from_orm_class(engine: Engine,
                                ormclass: DeclarativeMeta,
                                without_constraints: bool = False) -> None:
    """
    From an SQLAlchemy ORM class, creates the database table via the specified
    engine, using a ``CREATE TABLE`` SQL (DDL) statement.

    Args:
        engine: SQLAlchemy :class:`Engine` object
        ormclass: SQLAlchemy ORM class
        without_constraints: don't add foreign key constraints
    """
    table = ormclass.__table__  # type: Table
    log.info("Creating table {} on engine {}{}", table.name,
             get_safe_url_from_engine(engine),
             " (omitting constraints)" if without_constraints else "")
    # https://stackoverflow.com/questions/19175311/how-to-create-only-one-table-with-sqlalchemy  # noqa
    if without_constraints:
        include_foreign_key_constraints = []
    else:
        include_foreign_key_constraints = None  # the default
    creator = CreateTable(
        table, include_foreign_key_constraints=include_foreign_key_constraints)
    creator.execute(bind=engine)
Exemplo n.º 5
0
def merge_camcops_db(
    src: str,
    echo: bool,
    report_every: int,
    dummy_run: bool,
    info_only: bool,
    default_group_id: Optional[int],
    default_group_name: Optional[str],
    groupnum_map: Dict[int, int],
    whichidnum_map: Dict[int, int],
    skip_export_logs: bool = True,
    skip_audit_logs: bool = True,
) -> None:
    """
    Merge an existing database (with a pre-v2 or later structure) into a
    comtemporary CamCOPS database.

    Args:
        src:
            source database SQLAlchemy URL

        echo:
            echo the SQL that is produced?

        report_every:
            provide a progress report every *n* records

        dummy_run:
            don't alter the destination database

        info_only:
            show info, then stop

        default_group_id:
            integer group ID (in the destination database) to use for source
            records that have no group (because they come from a very old
            source database) but need one

        default_group_name:
            group name (in the destination database) to use for source
            records that have no group (because they come from a very old
            source database) but need one

        groupnum_map:
            dictionary mapping group ID values from the source database to
            the destination database

        whichidnum_map:
            dictionary mapping ``which_idnum`` values from the source database
            to the destination database

        skip_export_logs:
            skip export log tables

        skip_audit_logs:
            skip audit log table

    """
    req = get_command_line_request()  # requires manual COMMIT; see below
    src_engine = create_engine(src, echo=echo, pool_pre_ping=True)
    log.info("SOURCE: " + get_safe_url_from_engine(src_engine))
    log.info("DESTINATION: " + get_safe_url_from_engine(req.engine))
    log.info(
        "Destination ID number type map (source:destination) is: {!r}",
        whichidnum_map,
    )
    log.info("Group number type map (source:destination) is {!r}",
             groupnum_map)

    # Delay the slow import until we've checked our syntax
    log.info("Loading all models...")
    # noinspection PyUnresolvedReferences
    import camcops_server.cc_modules.cc_all_models  # delayed import  # import side effects (ensure all models registered)  # noqa

    log.info("Models loaded.")

    # Now, any special dependencies?
    # From the point of view of translating any tablet-related fields, the
    # actual (server) PK values are irrelevant; all relationships will be
    # identical if you change any PK (not standard database practice, but
    # convenient here).
    # The dependencies that do matter are server-side things, like user_id
    # variables.

    # For debugging only, some junk:
    # test_dependencies = [
    #     TableDependency(parent_tablename="patient",
    #                     child_tablename="_dirty_tables")
    # ]

    # -------------------------------------------------------------------------
    # Tables to skip
    # -------------------------------------------------------------------------

    skip_tables = [
        # Transient stuff we don't want to copy across, or wouldn't want to
        # overwrite the destination with, or where the PK structure has
        # changed and we don't care about old data:
        TableIdentity(tablename=x) for x in (
            CamcopsSession.__tablename__,
            DirtyTable.__tablename__,
            ServerSettings.__tablename__,
            SecurityAccountLockout.__tablename__,
            SecurityLoginFailure.__tablename__,
            UserGroupMembership.__tablename__,
            group_group_table.name,
        )
    ]

    # Tedious and bulky stuff the user may want to skip:
    if skip_export_logs:
        skip_tables.extend([
            TableIdentity(tablename=x) for x in (
                Email.__tablename__,
                ExportRecipient.__tablename__,
                ExportedTask.__tablename__,
                ExportedTaskEmail.__tablename__,
                ExportedTaskFileGroup.__tablename__,
                ExportedTaskHL7Message.__tablename__,
            )
        ])
    if skip_audit_logs:
        skip_tables.append(TableIdentity(tablename=AuditEntry.__tablename__))

    # -------------------------------------------------------------------------
    # Initial operations on SOURCE database
    # -------------------------------------------------------------------------

    src_tables = get_table_names(src_engine)
    skip_tables += get_skip_tables(src_tables=src_tables)
    src_iddefs = get_src_iddefs(src_engine, src_tables)
    log.info("Source ID number definitions: {!r}", src_iddefs)

    # -------------------------------------------------------------------------
    # Initial operations on DESTINATION database
    # -------------------------------------------------------------------------
    dst_session = req.dbsession
    # So that system users get the first ID (cosmetic!):
    _ = User.get_system_user(dbsession=dst_session)
    _ = Device.get_server_device(dbsession=dst_session)

    # -------------------------------------------------------------------------
    # Set up source-to-destination mappings
    # -------------------------------------------------------------------------

    # Map source to destination ID number types
    for src_which_idnum, dest_which_idnum in whichidnum_map.items():
        assert isinstance(src_which_idnum, int)
        assert isinstance(dest_which_idnum, int)
        src_iddef = src_iddefs[src_which_idnum]
        dst_iddef = ensure_dest_iddef_exists(dest_which_idnum, dst_session)
        ensure_no_iddef_clash(src_iddef, dst_iddef)

    # Map source to destination group numbers
    for src_groupnum, dest_groupnum in groupnum_map.items():
        assert isinstance(src_groupnum, int)
        assert isinstance(dest_groupnum, int)
        _ = get_dst_group(dest_groupnum, dst_session)

    # -------------------------------------------------------------------------
    # Merge
    # -------------------------------------------------------------------------

    # Merge! It's easy...
    trcon_info = dict(
        default_group_id=default_group_id,
        default_group_name=default_group_name,
        src_iddefs=src_iddefs,
        whichidnum_map=whichidnum_map,
        groupnum_map=groupnum_map,
    )
    merge_db(
        base_class=Base,
        src_engine=src_engine,
        dst_session=dst_session,
        allow_missing_src_tables=True,
        allow_missing_src_columns=True,
        translate_fn=translate_fn,
        skip_tables=skip_tables,
        only_tables=None,
        tables_to_keep_pks_for=None,
        # extra_table_dependencies=test_dependencies,
        extra_table_dependencies=None,
        dummy_run=dummy_run,
        info_only=info_only,
        report_every=report_every,
        flush_per_table=True,
        flush_per_record=False,
        commit_with_flush=False,
        commit_at_end=True,
        prevent_eager_load=True,
        trcon_info=trcon_info,
    )

    # -------------------------------------------------------------------------
    # Postprocess
    # -------------------------------------------------------------------------

    postprocess(src_engine=src_engine, dst_session=dst_session)

    # -------------------------------------------------------------------------
    # Done
    # -------------------------------------------------------------------------

    dst_session.commit()