コード例 #1
0
def get_client(url=None, auth_token=None, username=None, password=None):
    """Return Mergin client."""
    if auth_token is not None:
        try:
            mc = MerginClient(url, auth_token=auth_token)
        except ClientError as e:
            click.secho(str(e), fg="red")
            return None
        # Check if the token has expired or is just about to expire
        delta = mc._auth_session["expire"] - datetime.now(timezone.utc)
        if delta.total_seconds() > 5:
            return mc
    if username and password:
        auth_token = get_token(url, username, password)
        if auth_token is None:
            return None
        mc = MerginClient(url, auth_token=f"Bearer {auth_token}")
    else:
        click.secho(
            "Missing authorization data.\n"
            "Either set environment variables (MERGIN_USERNAME and MERGIN_PASSWORD) "
            "or specify --username / --password options.\n"
            "Note: if --username is specified but password is missing you will be prompted for password.",
            fg="red",
        )
        return None
    return mc
コード例 #2
0
def get_token(url, username, password):
    """Get authorization token for given user and password."""
    mc = MerginClient(url)
    if not mc.is_server_compatible():
        click.secho(str(
            "This client version is incompatible with server, try to upgrade"),
                    fg="red")
        return None
    try:
        session = mc.login(username, password)
    except LoginError as e:
        click.secho("Unable to log in: " + str(e), fg="red")
        return None
    return session["token"]
コード例 #3
0
def login(url, login, password):
    """Fetch new authentication token. If no URL is specified, the public Mergin instance will be used."""
    c = MerginClient(url)
    click.echo("Mergin URL: " + c.url)
    if not c.is_server_compatible():
        click.secho(str('This client version is incompatible with server, try to upgrade'), fg='red')
        return
    try:
        session = c.login(login, password)
    except LoginError as e:
        click.secho('Unable to log in: ' + str(e), fg='red')
        return

    print('export MERGIN_URL="%s"' % c.url)
    print('export MERGIN_AUTH="%s"' % session['token'])
コード例 #4
0
def _init_client():
    url = os.environ.get('MERGIN_URL')
    auth_token = os.environ.get('MERGIN_AUTH')
    if auth_token is None:
        click.secho(
            "Missing authorization token: please run 'login' first and set MERGIN_AUTH env variable",
            fg='red')
        return None
    return MerginClient(url, auth_token='Bearer {}'.format(auth_token))
コード例 #5
0
ファイル: dbsync.py プロジェクト: bartverhaar/mergin-db-sync
def create_mergin_client():
    """ Create instance of MerginClient"""
    _check_has_password()
    try:
        return MerginClient(config.mergin_url,
                            login=config.mergin_username,
                            password=config.mergin_password,
                            plugin_version=f"DB-sync/{__version__}")
    except LoginError as e:
        # this could be auth failure, but could be also server problem (e.g. worker crash)
        raise DbSyncError(
            f"Unable to log in to Mergin: {str(e)} \n\n" +
            "Have you specified correct credentials in configuration file?")
    except ClientError as e:
        # this could be e.g. DNS error
        raise DbSyncError("Mergin client error: " + str(e))
コード例 #6
0
def _init_client():
    url = os.environ.get('MERGIN_URL')
    auth_token = os.environ.get('MERGIN_AUTH')
    if auth_token is None:
        click.secho("Missing authorization token: please run 'login' first and set MERGIN_AUTH env variable", fg='red')
        return None

    mc = MerginClient(url, auth_token='Bearer {}'.format(auth_token))

    # check whether the token has not expired already (normally it expires in 12 hours)
    from datetime import datetime, timezone
    delta = mc._auth_session['expire'] - datetime.now(timezone.utc)
    if delta.total_seconds() < 0:
        click.secho("Access token has expired: please run 'login' again and set MERGIN_AUTH env variable", fg='red')
        return None

    return mc
コード例 #7
0
def _print_unhandled_exception():
    """ Outputs details of an unhandled exception that is being handled right now """
    click.secho("Unhandled exception!", fg="red")
    for line in traceback.format_exception(*sys.exc_info()):
        click.echo(line)


@click.group(
    epilog=
    f"Copyright (C) 2019-2021 Lutra Consulting\n\n(mergin-py-client v{__version__} / pygeodiff v{GeoDiff().version()})"
)
@click.option(
    "--url",
    envvar="MERGIN_URL",
    default=MerginClient.default_url(),
    help=f"Mergin server URL. Default is: {MerginClient.default_url()}",
)
@click.option("--auth-token",
              envvar="MERGIN_AUTH",
              help="Mergin authentication token string")
@click.option("--username", envvar="MERGIN_USERNAME")
@click.option("--password",
              cls=OptionPasswordIfUser,
              prompt=True,
              hide_input=True,
              envvar="MERGIN_PASSWORD")
@click.pass_context
def cli(ctx, url, auth_token, username, password):
    """
    Command line interface for the Mergin client module.
コード例 #8
0
def mc():
    assert SERVER_URL and API_USER and USER_PWD
    #assert SERVER_URL and SERVER_URL.rstrip('/') != 'https://public.cloudmergin.com' and API_USER and USER_PWD
    return MerginClient(SERVER_URL, login=API_USER, password=USER_PWD)
コード例 #9
0
ファイル: dbsync.py プロジェクト: kartoza/mergin-db-sync
def dbsync_init(from_gpkg=True):
    """ Initialize the dbsync so that it is possible to do two-way sync between Mergin and a database """

    # let's start with various environment checks to make sure
    # the environment is set up correctly before doing any work

    if os.path.exists(config.project_working_dir):
        raise DbSyncError("The project working directory already exists: " +
                          config.project_working_dir)

    print("Connecting to the database...")
    try:
        conn = psycopg2.connect(config.db_conn_info)
    except psycopg2.Error as e:
        raise DbSyncError("Unable to connect to the database: " + str(e))

    if _check_schema_exists(conn, config.db_schema_base):
        raise DbSyncError("The base schema already exists: " +
                          config.db_schema_base)

    if from_gpkg:
        if _check_schema_exists(conn, config.db_schema_modified):
            raise DbSyncError("The 'modified' schema already exists: " +
                              config.db_schema_modified)
    else:
        if not _check_schema_exists(conn, config.db_schema_modified):
            raise DbSyncError("The 'modified' schema does not exist: " +
                              config.db_schema_modified)

    _check_has_password()
    print("Logging in to Mergin...")
    try:
        mc = MerginClient(config.mergin_url,
                          login=config.mergin_username,
                          password=config.mergin_password)
    except LoginError:
        raise DbSyncError(
            "Unable to log in to Mergin: have you specified correct credentials in configuration file?"
        )

    # download the Mergin project
    print("Download Mergin project " + config.mergin_project_name + " to " +
          config.project_working_dir)
    mc.download_project(config.mergin_project_name, config.project_working_dir)

    _check_has_working_dir()

    gpkg_full_path = os.path.join(config.project_working_dir,
                                  config.mergin_sync_file)
    if from_gpkg:
        if not os.path.exists(gpkg_full_path):
            raise DbSyncError("The input GPKG file does not exist: " +
                              gpkg_full_path)
    else:
        if os.path.exists(gpkg_full_path):
            raise DbSyncError("The output GPKG file exists already: " +
                              gpkg_full_path)

    # check there are no pending changes on server (or locally - which should never happen)
    status_pull, status_push, _ = mc.project_status(config.project_working_dir)
    if status_pull['added'] or status_pull['updated'] or status_pull['removed']:
        raise DbSyncError(
            "There are pending changes on server - need to pull them first: " +
            str(status_pull))
    if status_push['added'] or status_push['updated'] or status_push['removed']:
        raise DbSyncError(
            "There are pending changes in the local directory - that should never happen! "
            + str(status_push))

    if from_gpkg:
        # we have an existing GeoPackage in our Mergin project and we want to initialize database

        # COPY: gpkg -> modified
        _geodiff_make_copy("sqlite", "", gpkg_full_path, config.db_driver,
                           config.db_conn_info, config.db_schema_modified)

        # COPY: modified -> base
        _geodiff_make_copy(config.db_driver, config.db_conn_info,
                           config.db_schema_modified, config.db_driver,
                           config.db_conn_info, config.db_schema_base)

    else:
        # we have an existing schema in database with tables and we want to initialize geopackage
        # within our a Mergin project

        # COPY: modified -> base
        _geodiff_make_copy(config.db_driver, config.db_conn_info,
                           config.db_schema_modified, config.db_driver,
                           config.db_conn_info, config.db_schema_base)

        # COPY: modified -> gpkg
        _geodiff_make_copy(config.db_driver, config.db_conn_info,
                           config.db_schema_modified, "sqlite", "",
                           gpkg_full_path)

        # upload gpkg to mergin (client takes care of storing metadata)
        mc.push_project(config.project_working_dir)
コード例 #10
0
ファイル: dbsync.py プロジェクト: kartoza/mergin-db-sync
def dbsync_push():
    """ Take changes in the 'modified' schema in the database and push them to Mergin """

    tmp_dir = tempfile.gettempdir()
    tmp_changeset_file = os.path.join(tmp_dir, 'dbsync-push-base2our')
    if os.path.exists(tmp_changeset_file):
        os.remove(tmp_changeset_file)

    _check_has_working_dir()
    _check_has_sync_file()
    _check_has_password()

    try:
        mc = MerginClient(config.mergin_url,
                          login=config.mergin_username,
                          password=config.mergin_password)
        status_pull, status_push, _ = mc.project_status(
            config.project_working_dir)
    except LoginError as e:
        # this could be auth failure, but could be also server problem (e.g. worker crash)
        raise DbSyncError("Mergin log in error: " + str(e))
    except ClientError as e:
        raise DbSyncError("Mergin client error: " + str(e))

    # check there are no pending changes on server (or locally - which should never happen)
    if status_pull['added'] or status_pull['updated'] or status_pull['removed']:
        raise DbSyncError(
            "There are pending changes on server - need to pull them first: " +
            str(status_pull))
    if status_push['added'] or status_push['updated'] or status_push['removed']:
        raise DbSyncError(
            "There are pending changes in the local directory - that should never happen! "
            + str(status_push))

    conn = psycopg2.connect(config.db_conn_info)

    if not _check_schema_exists(conn, config.db_schema_base):
        raise DbSyncError("The base schema does not exist: " +
                          config.db_schema_base)
    if not _check_schema_exists(conn, config.db_schema_modified):
        raise DbSyncError("The 'modified' schema does not exist: " +
                          config.db_schema_modified)

    # get changes in the DB
    _geodiff_create_changeset(config.db_driver, config.db_conn_info,
                              config.db_schema_base, config.db_schema_modified,
                              tmp_changeset_file)

    if os.path.getsize(tmp_changeset_file) == 0:
        print("No changes in the database.")
        return

    # summarize changes
    summary = _geodiff_list_changes_summary(tmp_changeset_file)
    _print_changes_summary(summary)

    # write changes to the local geopackage
    print("Writing DB changes to working dir...")
    gpkg_full_path = os.path.join(config.project_working_dir,
                                  config.mergin_sync_file)
    _geodiff_apply_changeset("sqlite", "", gpkg_full_path, tmp_changeset_file)

    # write to the server
    try:
        mc.push_project(config.project_working_dir)
    except ClientError as e:
        # TODO: should we do some cleanup here? (undo changes in the local geopackage?)
        raise DbSyncError("Mergin client error on push: " + str(e))

    print("Pushed new version to Mergin: " + _get_project_version())

    # update base schema in the DB
    print("Updating DB base schema...")
    _geodiff_apply_changeset(config.db_driver, config.db_conn_info,
                             config.db_schema_base, tmp_changeset_file)

    print("Push done!")
コード例 #11
0
ファイル: dbsync.py プロジェクト: kartoza/mergin-db-sync
def dbsync_status():
    """ Figure out if there are any pending changes in the database or in Mergin """

    _check_has_working_dir()
    _check_has_sync_file()
    _check_has_password()

    # get basic information
    mp = MerginProject(config.project_working_dir)
    if mp.geodiff is None:
        raise DbSyncError(
            "Mergin client installation problem: geodiff not available")
    status_push = mp.get_push_changes()
    if status_push['added'] or status_push['updated'] or status_push['removed']:
        raise DbSyncError(
            "Pending changes in the local directory - that should never happen! "
            + str(status_push))

    project_path = mp.metadata["name"]
    local_version = mp.metadata["version"]
    print("Working directory " + config.project_working_dir)
    print("Mergin project " + project_path + " at local version " +
          local_version)
    print("")
    print("Checking status...")

    # check if there are any pending changes on server
    try:
        mc = MerginClient(config.mergin_url,
                          login=config.mergin_username,
                          password=config.mergin_password)
        server_info = mc.project_info(project_path, since=local_version)
    except LoginError as e:
        # this could be auth failure, but could be also server problem (e.g. worker crash)
        raise DbSyncError("Mergin log in error: " + str(e))
    except ClientError as e:
        raise DbSyncError("Mergin client error: " + str(e))

    print("Server is at version " + server_info["version"])

    status_pull = mp.get_pull_changes(server_info["files"])
    if status_pull['added'] or status_pull['updated'] or status_pull['removed']:
        print("There are pending changes on server:")
        _print_mergin_changes(status_pull)
    else:
        print("No pending changes on server.")

    print("")
    conn = psycopg2.connect(config.db_conn_info)

    if not _check_schema_exists(conn, config.db_schema_base):
        raise DbSyncError("The base schema does not exist: " +
                          config.db_schema_base)
    if not _check_schema_exists(conn, config.db_schema_modified):
        raise DbSyncError("The 'modified' schema does not exist: " +
                          config.db_schema_modified)

    # get changes in the DB
    tmp_dir = tempfile.gettempdir()
    tmp_changeset_file = os.path.join(tmp_dir, 'dbsync-status-base2our')
    if os.path.exists(tmp_changeset_file):
        os.remove(tmp_changeset_file)
    _geodiff_create_changeset(config.db_driver, config.db_conn_info,
                              config.db_schema_base, config.db_schema_modified,
                              tmp_changeset_file)

    if os.path.getsize(tmp_changeset_file) == 0:
        print("No changes in the database.")
    else:
        print("There are changes in DB")
        # summarize changes
        summary = _geodiff_list_changes_summary(tmp_changeset_file)
        _print_changes_summary(summary)
コード例 #12
0
ファイル: dbsync.py プロジェクト: kartoza/mergin-db-sync
def dbsync_pull():
    """ Downloads any changes from Mergin and applies them to the database """

    _check_has_working_dir()
    _check_has_sync_file()
    _check_has_password()

    try:
        mc = MerginClient(config.mergin_url,
                          login=config.mergin_username,
                          password=config.mergin_password)
        status_pull, status_push, _ = mc.project_status(
            config.project_working_dir)
    except LoginError as e:
        # this could be auth failure, but could be also server problem (e.g. worker crash)
        raise DbSyncError("Mergin log in error: " + str(e))
    except ClientError as e:
        # this could be e.g. DNS error
        raise DbSyncError("Mergin client error: " + str(e))

    if not status_pull['added'] and not status_pull[
            'updated'] and not status_pull['removed']:
        print("No changes on Mergin.")
        return
    if status_push['added'] or status_push['updated'] or status_push['removed']:
        raise DbSyncError(
            "There are pending changes in the local directory - that should never happen! "
            + str(status_push))

    gpkg_basefile = os.path.join(config.project_working_dir, '.mergin',
                                 config.mergin_sync_file)
    gpkg_basefile_old = gpkg_basefile + "-old"

    # make a copy of the basefile in the current version (base) - because after pull it will be set to "their"
    shutil.copy(gpkg_basefile, gpkg_basefile_old)

    tmp_dir = tempfile.gettempdir()
    tmp_base2our = os.path.join(tmp_dir, 'dbsync-pull-base2our')
    tmp_base2their = os.path.join(tmp_dir, 'dbsync-pull-base2their')

    # find out our local changes in the database (base2our)
    _geodiff_create_changeset(config.db_driver, config.db_conn_info,
                              config.db_schema_base, config.db_schema_modified,
                              tmp_base2our)

    needs_rebase = False
    if os.path.getsize(tmp_base2our) != 0:
        needs_rebase = True
        summary = _geodiff_list_changes_summary(tmp_base2our)
        _print_changes_summary(summary, "DB Changes:")

    try:
        mc.pull_project(config.project_working_dir)  # will do rebase as needed
    except ClientError as e:
        # TODO: do we need some cleanup here?
        raise DbSyncError("Mergin client error on pull: " + str(e))

    print("Pulled new version from Mergin: " + _get_project_version())

    # simple case when there are no pending local changes - just apply whatever changes are coming
    _geodiff_create_changeset("sqlite", "", gpkg_basefile_old, gpkg_basefile,
                              tmp_base2their)

    # summarize changes
    summary = _geodiff_list_changes_summary(tmp_base2their)
    _print_changes_summary(summary, "Mergin Changes:")

    if not needs_rebase:
        print("Applying new version [no rebase]")
        _geodiff_apply_changeset(config.db_driver, config.db_conn_info,
                                 config.db_schema_base, tmp_base2their)
        _geodiff_apply_changeset(config.db_driver, config.db_conn_info,
                                 config.db_schema_modified, tmp_base2their)
    else:
        print("Applying new version [WITH rebase]")
        tmp_conflicts = os.path.join(tmp_dir, 'dbsync-pull-conflicts')
        _geodiff_rebase(config.db_driver, config.db_conn_info,
                        config.db_schema_base, config.db_schema_modified,
                        tmp_base2their, tmp_conflicts)
        _geodiff_apply_changeset(config.db_driver, config.db_conn_info,
                                 config.db_schema_base, tmp_base2their)

    os.remove(gpkg_basefile_old)

    print("Pull done!")