# Source code for sbws.core.generate

    import logging
    import os
    from argparse import ArgumentDefaultsHelpFormatter
    from math import ceil

    from sbws.globals import (
        DAY_SECS,
        GENERATE_PERIOD,
        NUM_MIN_RESULTS,
        PROP276_ROUND_DIG,
        SBWS_SCALE_CONSTANT,
        SBWS_SCALING,
        TORFLOW_BW_MARGIN,
        TORFLOW_SCALING,
        fail_hard,
    )
    from sbws.lib import destination
    from sbws.lib.resultdump import load_recent_results_in_datadir
    from sbws.lib.v3bwfile import V3BWFile
    from sbws.util.timestamp import now_fname

    log = logging.getLogger(__name__)

    def gen_parser(sub):
        d = (
            "Generate a v3bw file based on recent results. A v3bw file is the "
            "file Tor directory authorities want to read and base their "
            "bandwidth votes on. "
            "To avoid inconsistent reads, configure tor with "
            '"V3BandwidthsFile /path/to/latest.v3bw". '
            "(latest.v3bw is an atomically created symlink in the same "
            "directory as output.) "
            "If the file is transferred to another host, it should be written to "
            "a temporary path, then renamed to the V3BandwidthsFile path.\n"
            "The default scaling method is torflow's one. To use different"
            "scaling methods or no scaling, see the options."
        )
        p = sub.add_parser(
            "generate",
            description=d,
            formatter_class=ArgumentDefaultsHelpFormatter,
        )
        p.add_argument(
            "--output",
            default=None,
            type=str,
            help="If specified, write the v3bw here instead of what is"
            "specified in the configuration",
        )
        # The reason for --scale-constant defaulting to 7500 is because at one
        # time, torflow happened to generate output that averaged to 7500 bw units
        # per relay. We wanted the ability to try to be like torflow. See
        # https://lists.torproject.org/pipermail/tor-dev/2018-March/013049.html
        p.add_argument(
            "--scale-constant",
            default=SBWS_SCALE_CONSTANT,
            type=int,
            help="When scaling bw weights, scale them using this const "
            "multiplied by the number of measured relays",
        )
        p.add_argument(
            "--scale-sbws",
            action="store_true",
            help="If specified, do not use bandwidth values as they "
            "are, but scale them such that we have a budget of "
            "scale_constant * num_measured_relays = bandwidth to give "
            "out, and we do so proportionally",
        )
        p.add_argument(
            "-t",
            "--scale-torflow",
            action="store_true",
            default=True,
            help="If specified, scale measurements using torflow's "
            "method. This option is kept for compatibility with older "
            "versions and it is silently ignored, since it is the "
            "default.",
        )
        p.add_argument(
            "-w",
            "--raw",
            action="store_true",
            help="If specified, do use bandwidth raw measurements "
            "without any scaling.",
        )
        p.add_argument(
            "-m",
            "--torflow-bw-margin",
            default=TORFLOW_BW_MARGIN,
            type=float,
            help="Cap maximum bw when scaling as Torflow. ",
        )
        p.add_argument(
            "-r",
            "--round-digs",
            "--torflow-round-digs",
            default=PROP276_ROUND_DIG,
            type=int,
            help="Number of most significant digits to round bw.",
        )
        p.add_argument(
            "-p",
            "--secs-recent",
            default=None,
            type=int,
            help="How many secs in the past are results being "
            "still considered. Default is {} secs. If not scaling "
            "as Torflow the default is data_period in the "
            "configuration.".format(GENERATE_PERIOD),
        )
        p.add_argument(
            "-a",
            "--secs-away",
            default=DAY_SECS,
            type=int,
            help="How many secs results have to be away from each " "other.",
        )
        p.add_argument(
            "-n",
            "--min-num",
            default=NUM_MIN_RESULTS,
            type=int,
            help="Minimum number of a results to consider them.",
        )
        return p

    def main(args, conf):
        os.makedirs(conf.getpath("paths", "v3bw_dname"), exist_ok=True)

        datadir = conf.getpath("paths", "datadir")
        if not os.path.isdir(datadir):
            fail_hard("%s does not exist", datadir)
        if args.scale_constant < 1:
            fail_hard("--scale-constant must be positive")
        if args.torflow_bw_margin < 0:
            fail_hard("toflow-bw-margin must be major than 0.")
        if args.scale_sbws:
            scaling_method = SBWS_SCALING
        elif args.raw:
            scaling_method = None
        else:
            # sbws will scale as torflow until we have a better algorithm for
            # scaling (#XXX)
            scaling_method = TORFLOW_SCALING
        if args.secs_recent:
            fresh_days = ceil(args.secs_recent / 24 / 60 / 60)
        elif scaling_method == TORFLOW_SCALING:
            fresh_days = ceil(GENERATE_PERIOD / 24 / 60 / 60)
        else:
            fresh_days = conf.getint("general", "data_period")
        reset_bw_ipv4_changes = conf.getboolean("general", "reset_bw_ipv4_changes")
        reset_bw_ipv6_changes = conf.getboolean("general", "reset_bw_ipv6_changes")
        results = load_recent_results_in_datadir(
            fresh_days,
            datadir,
            on_changed_ipv4=reset_bw_ipv4_changes,
            on_changed_ipv6=reset_bw_ipv6_changes,
        )
        if len(results) < 1:
            log.warning(
                "No recent results, so not generating anything. (Have you "
                "ran sbws scanner recently?)"
            )
            return
        state_fpath = conf.getpath("paths", "state_fname")
        consensus_path = os.path.join(
            conf.getpath("tor", "datadir"), "cached-consensus"
        )
        # Accept None as scanner_country to be compatible with older versions.
        scanner_country = conf["scanner"].get("country")
        destinations_countries = destination.parse_destinations_countries(conf)
        bw_file = V3BWFile.from_results(
            results,
            scanner_country,
            destinations_countries,
            state_fpath,
            args.scale_constant,
            scaling_method,
            torflow_cap=args.torflow_bw_margin,
            round_digs=args.round_digs,
            secs_recent=args.secs_recent,
            secs_away=args.secs_away,
            min_num=args.min_num,
            consensus_path=consensus_path,
        )

        output = args.output or conf.getpath("paths", "v3bw_fname").format(
            now_fname()
        )
        bw_file.write(output)
        bw_file.info_stats