"""
Created on Sun Jun  9 11:02:05 2019

@author: patrickmcfarlane

salaries.py contains the functions
to gather NBA salary information
"""

from requests import get

SALARY_URL = "http://example.com"

URL_TO_ID_NBA = {'http://example.com/': '1610612761',
                 'http://example.com/': '1610612743',
                 'http://example.com/': '1610612765',
                 'http://example.com/': '1610612740',
                 'http://example.com/': '1610612749',
                 'http://example.com/': '1610612744',
                 'http://example.com/': '1610612759',
                 'http://example.com/': '1610612757',
                 'http://example.com/': '1610612746',
                 'http://example.com/': '1610612742',
                 'http://example.com/': '1610612763',
                 'http://example.com/': '1610612755',
                 'http://example.com/': '1610612738',
                 'http://example.com/': '1610612750',
                 'http://example.com/': '1610612766',
                 'http://example.com/': '1610612754',
                 'http://example.com/': '1610612753',
                 'http://example.com/': '1610612748',
                 'http://example.com/': '1610612745',
                 'http://example.com/': '1610612758',
                 'http://example.com/': '1610612762',
                 'http://example.com/': '1610612751',
                 'http://example.com/': '1610612737',
                 'http://example.com/': '1610612756',
                 'http://example.com/': '1610612764',
                 'http://example.com/': '1610612752',
                 'http://example.com/': '1610612760',
                 'http://example.com/': '1610612747',
                 'http://example.com/': '1610612739',
                 'http://example.com/': '1610612741'}

def salary_columns(html_text, context='current'):
    """ salary_columns returns the column names
    for the salary information

    @param **html_text** (*str*): String of the HTML response
    from SALARY_URL

    @param **context** (*str*): 'current' signifies that current
        and future salaries are being pulled. Otherwise, historical
        values are returned

    Returns:

        **html_text** (*str*): Truncated string of the HTML
            response from a Hoopshype URL with the column information
            removed

        **column_list** (*list*): List of column names for
            salary information
    """
    if context == 'current':
        col_count = 6
    else:
        col_count = 2

    column_list = []
    for col_count in range(0, col_count):
        start_ind = html_text.find('>') + 1
        end_ind = html_text.find('	')
        column_list.append(html_text[start_ind:end_ind])
        html_text = html_text[end_ind + 5:]

    return html_text, column_list

def get_option(option):
    """ get_option returns the type of option (if any) applied
    to that year's salary

    @param **option** (*str*): Sting of color indicators that
        correspond to yearly options.

    Returns:

        **option** (*list*): One of '', 'Team',
        'Qualifying', 'Two-Way', 'Player'
    """

    if option == 'color:black':
        option = ''
    elif option == 'color:rgb(255, 0, 0)':
        option = 'Team'
    elif option == 'color:rgb(0, 153, 0)':
        option = 'Qualifying'
    elif option == 'color:rgb(168, 0, 212)':
        option = 'Two-Way'
    elif option == 'color:rgb(4, 134, 176)':
        option = 'Player'
    else:
        option = ''

    return option

def team_salary_values(html_text):
    """ team_salary_values returns a dictionary of
    salary information keyed by NBA team ID

    @param **html_text** (*str*): String of the HTML response
    from SALARY_URL

    Returns:

        **team_salaries** (*dict*): Dictionary keyed by NBA
            team ID with a list of yearly salaries (followed
            by team salary URL) as values
    """
    value_str = 'data-value="'
    team_salaries = {}
    nba_teams = 30

    for teams in range(0, nba_teams):
        team_list = []
        start_ind = html_text.find('<a')
        end_ind = html_text.find('')
        team_key = html_text[start_ind + 2: end_ind]
        team_key = team_key[team_key.find('href="') + 6:team_key.find('">\n')]
        html_text = html_text[end_ind + 4:]
        for col_count in range(0, 6):
            start_ind = html_text.find(value_str) + len(value_str)
            end_ind = html_text.find('">')
            team_list.append(int(html_text[start_ind:end_ind]))
            html_text = html_text[end_ind + 2:]
        team_list.append(team_key)
        team_salaries[URL_TO_ID_NBA[team_key]] = team_list

    return team_salaries

def team_player_salary_values(html_text):
    """ team_player_salary_values returns a dictionary of
    salary information keyed by Hoopshype player ID

    @param **html_text** (*str*): String of the HTML response
    from a Hoopshype team URL

    Returns:

        **player_salaries** (*dict*): Dictionary keyed by Hoopshype
            player ID with a list of yearly salaries (followed
            by player salary URL) as values
    """
    value_str = 'data-value="'
    option_str = 'style="'
    player_salaries = {}
    url_val = html_text.find('Totals	')

    while url_val < total_val:
        player_list = []
        option_list = []
        start_ind = html_text.find('<a')
        end_ind = html_text.find('')
        play_key = html_text[start_ind + 2: end_ind]
        play_key = play_key[play_key.find('href="') + 6:play_key.find('">\n')]
        player_key = play_key.split('/')[-3]
        html_text = html_text[end_ind + 4:]
        for col_count in range(0, 2):
            start_ind = html_text.find(option_str) + len(option_str)
            end_ind = html_text.find('" ')
            option = get_option(html_text[start_ind:end_ind])
            option_list.append(option)
            html_text = html_text[end_ind + 2:]

            start_ind = html_text.find(value_str) + len(value_str)
            end_ind = html_text.find('">')

player_list.append(salary_value)
            html_text = html_text[end_ind + 2:]

        player_list.append(play_key)
        player_salaries[player_key] = {}
        player_salaries[player_key]['salary'] = player_list
        player_salaries[player_key]['options'] = option_list

        total_val = html_text.find('class="name">Totals	')
        url_val = html_text.find('<a')

    return player_salaries

def historical_team_salary_values(html_text, season):
    """ historical_team_salary_values returns a dictionary of
    salary information keyed by NBA team ID

    @param **html_text** (*str*): String of the HTML response
    from SALARY_URL + season
    @param **season** (*str*): String of season in YYYY-ZZZZ
    format

    Returns:

        **team_salaries** (*dict*): Dictionary keyed by NBA
            team ID with a list of yearly salaries (followed
            by team salary URL) as values
    """
    value_str = 'data-value="'
    team_salaries = {}
    nba_teams = 30

    for teams in range(0, nba_teams):
        team_list = []
        start_ind = html_text.find('<a')
        end_ind = html_text.find('')
        team_key = html_text[start_ind + 2: end_ind]
        team_key = team_key[team_key.find('href="') + 6:team_key.find('">\n')]
        html_text = html_text[end_ind + 4:]
        for col_count in range(0, 2):
            start_ind = html_text.find(value_str) + len(value_str)
            end_ind = html_text.find('">')
            team_list.append(int(html_text[start_ind:end_ind]))
            html_text = html_text[end_ind + 2:]
        team_list.append(team_key)
        team_key = team_key.replace(season + '/', '')
        team_salaries[URL_TO_ID_NBA[team_key]] = team_list

    return team_salaries

def get_team_salary():
    """ This function pulls team salary information for six seasons
    from `hoopshype.com <https://hoopshype.com/salaries/>`

    Returns:

        **team_salaries** (*dict*): Dictionary keyed by NBA
            team ID with a list of yearly salaries (followed
            by team salary URL) as values

        **column_list** (*list*): List of column names for team
            salary information
    """

    api_resp = get(SALARY_URL)
    html_text = api_resp.text
    sorted_str = 'hh-salaries-sorted'

    table_index = html_text.find(sorted_str)
    html_text = html_text[table_index:]
    html_text, column_list = salary_columns(html_text, 'current')
    column_list.append('url')

    team_salaries = team_salary_values(html_text)

    return team_salaries, column_list

def get_historical_team_salary(season):
    """ This function pulls historical team salary
    information for the given season
    from `hoopshype.com <https://hoopshype.com/salaries/>`

    @param **season** (*str*): Season in YYYY-ZZZZ format. For example,
        '2017-2018' corresponds to the 2017-2018 NBA season

    Returns:

        **team_salaries** (*dict*): Dictionary keyed by NBA
            team ID with yearly salaries and inflation-adjusted
            yearly salaries (followed by team salary URL) as values

        **column_list** (*list*): List of column names for historical team
            salary information
    """

    api_resp = get(SALARY_URL + season)
    html_text = api_resp.text
    sorted_str = 'class="name">Team	'

    table_index = html_text.find(sorted_str) + len(sorted_str)
    html_text = html_text[table_index:]
    html_text, column_list = salary_columns(html_text, 'historical')
    column_list.append('url')

    team_salaries = historical_team_salary_values(html_text, season)

    return team_salaries, column_list

def player_salary_values(html_text):
    """ player_salary_values returns a dictionary of
    salary information keyed by Hoopshype player ID

    @param **html_text** (*str*): String of the HTML response
    from a Hoopshype team URL

    Returns:

        **player_salaries** (*dict*): Dictionary keyed by Hoopshype
            player ID with a list of yearly salaries (followed
            by player salary URL) as values
    """
    value_str = 'data-value="'
    option_str = 'style="'
    player_salaries = {}
    url_val = html_text.find('Totals	')

    while url_val < total_val:
        player_list = []
        option_list = []
        start_ind = html_text.find('<a')
        end_ind = html_text.find('')
        play_key = html_text[start_ind + 2: end_ind]
        play_key = play_key[play_key.find('href="') + 6:play_key.find('">\n')]
        player_key = play_key.split('/')[-3]
        html_text = html_text[end_ind + 4:]
        for col_count in range(0, 6):
            start_ind = html_text.find(option_str) + len(option_str)
            end_ind = html_text.find('" ')
            option = get_option(html_text[start_ind:end_ind])
            option_list.append(option)
            html_text = html_text[end_ind + 2:]

            start_ind = html_text.find(value_str) + len(value_str)
            end_ind = html_text.find('">')
            try:
                salary_value = int(html_text[start_ind:end_ind])
            except:
                salary_value = 0
            player_list.append(salary_value)
            html_text = html_text[end_ind + 2:]

        player_list.append(play_key)
        player_salaries[player_key] = {}
        player_salaries[player_key]['salary'] = player_list
        player_salaries[player_key]['options'] = option_list

        total_val = html_text.find('class="name">Totals	')
        url_val = html_text.find('<a')

    return player_salaries

def historical_player_salary_values(html_text):
    """ historical_player_salary_values returns a dictionary of
    salary information keyed by Hoopshype player ID

    @param **html_text** (*str*): String of the HTML response
    from a Hoopshype team URL

    Returns:

        **player_salaries** (*dict*): Dictionary keyed by Hoopshype
            player ID with a list of salary and inflation-adjusted
            salary (followed by player salary URL) as values
    """
    value_str = 'data-value="'
    option_str = 'style="'
    player_salaries = {}
    url_val = html_text.find('Totals	')

    while url_val < total_val:
        player_list = []
        start_ind = html_text.find('<a')
        end_ind = html_text.find('')
        play_key = html_text[start_ind + 2: end_ind]
        play_key = play_key[play_key.find('href="') + 6:play_key.find('">\n')]
        player_key = play_key.split('/')[-3]
        html_text = html_text[end_ind + 4:]
        for col_count in range(0, 2):
            start_ind = html_text.find(option_str) + len(option_str)
            end_ind = html_text.find('" ')
            html_text = html_text[end_ind + 2:]

            start_ind = html_text.find(value_str) + len(value_str)
            end_ind = html_text.find('">')
            try:
                salary_value = int(html_text[start_ind:end_ind])
            except:
                salary_value = 0
            player_list.append(salary_value)
            html_text = html_text[end_ind + 2:]

        player_list.append(play_key)
        player_salaries[player_key] = {}
        player_salaries[player_key]['salary'] = player_list

        total_val = html_text.find('class="name">Totals	')
        url_val = html_text.find('<a')

    return player_salaries

def get_player_salary(team_url):
    """ This function pulls player salary information for six seasons
    from `hoopshype.com <https://hoopshype.com/salaries/>`

    @param team_url (str): String of URL corresponding
    to a single team's salary

    Returns:

        **player_salaries** (*dict*): Dictionary keyed by Hoopshype
            player ID with a list of yearly salaries as values

        **column_list** (*list*): List of column names for team
            salary information
    """

    api_resp = get(team_url)
    html_text = api_resp.text
    sorted_str = 'hh-salaries-sorted'

    table_index = html_text.find(sorted_str)
    html_text = html_text[table_index:]
    html_text, column_list = salary_columns(html_text)
    column_list.append('url')

    player_salaries = player_salary_values(html_text)

    return player_salaries, column_list

def get_historical_player_salary(team_url, season):
    """ This function pulls historical player salary information for six seasons
    from `hoopshype.com <https://hoopshype.com/salaries/>`

    @param **team_url** (*str*): Hoopshype URL beginning with SALARY_URL
        appended with a season value. For example,
        'http://example.com/' is a valid
        **team_url**

    @param **season** (*str*): Season in YYYY-ZZZZ format. For example,
        '2017-2018' corresponds to the 2017-2018 NBA season
    Returns:

        **player_salaries** (*dict*): Dictionary keyed by Hoopshype
            player ID with a list of yearly salaries as values

        **column_list** (*list*): List of column names for team
            salary information
    """

    api_resp = get(team_url)
    html_text = api_resp.text
    sorted_str = 'class="name">Player	'

    table_index = html_text.find(sorted_str) + len(sorted_str)
    html_text = html_text[table_index:]
    html_text, column_list = salary_columns(html_text, season)
    column_list.append('url')

    player_salaries = historical_player_salary_values(html_text)

    return player_salaries, column_list

class TeamSalaries:
    """ The TeamSalaries class contains all resources needed
    to scrape team salary information from
    `hoopshype.com <https://hoopshype.com/salaries/>`.
    This class contains both team total salary as well as individual
    player breakdowns

    Attributes:

        **totals** (*dict*): Dictionary keyed by NBA
            team ID with a list of yearly salaries (followed
            by team salary URL) as values

        **totals_columns** (*list*): List of column names for team
            salary information

        **team_player_salaries** (*dict*): Dictionary keyed by both NBA
            team ID and Hoopshype player ID with values containing both
            salary and option information

        **team_player_columns** (*list*): List of column names for team-player
            salary information
    """

    def __init__(self):

        # First, total team salary information is pulled
        team_salaries, column_list = get_team_salary()
        self.totals = team_salaries
        self.totals_columns = column_list
        team_player_salary = {}
        for team_id in team_salaries:
            team_url = team_salaries[team_id][-1]
            player_salaries, column_list = get_player_salary(team_url)
            team_player_salary[team_id] = player_salaries

        self.team_player_salaries = team_player_salary
        self.team_player_columns = column_list

class HistoricalSalaries:
    """ The HistoricalSalaries class contains all resources needed
    to scrape team salary information from
    `hoopshype.com <https://hoopshype.com/salaries/>` for a given season.
    This class contains both team total salary as well as individual
    player breakdowns

    @param **season** (*str*): String of season in YYYY-ZZZZ
    format

    Attributes:

        **totals** (*dict*): Dictionary keyed by NBA
            team ID with a list of salaries and inflation-adjusted salaries
            (followed by team salary URL) as values

        **totals_columns** (*list*): List of column names for historical team
            salary information

        **team_player_salaries** (*dict*): Dictionary keyed by both NBA
            team ID and Hoopshype player ID with values containing
            salary information

        **team_player_columns** (*list*): List of column names for historical
            team-player salary information
    """

    def __init__(self, season='2017-2018'):

        # First, total team salary information is pulled
        team_salaries, column_list = get_historical_team_salary(season)
        self.totals = team_salaries
        self.totals_columns = column_list
        team_player_salary = {}
        for team_id in team_salaries:
            team_url = team_salaries[team_id][-1]
            player_salaries, column_list = get_historical_player_salary(team_url, season)
            team_player_salary[team_id] = player_salaries

        self.team_player_salaries = team_player_salary
        self.team_player_columns = column_list


def __init__(self, domain, record, filename):

import dns, dns.resolver

class dns_scan(object):

    def run(self, domain, record, filename):

        if filename is not None:
            file = open(filename, 'a')
            print("File opened\n"
              "Writing the information on the file..")
            ans = dns.resolver.query(domain, record)
            file.write(ans.response.to_text())
            print("Done.")
            file.close()
        else:
            print('Getting information..')
            ans = dns.resolver.query(domain, record)
            print(ans.response.to_text())

self.run(domain, record, filename)
        exit()


if filename is not None:
            file = open(filename, 'a')
            print("File opened\n"
              "Writing the information on the file..")
            ans = dns.resolver.query(domain, record)
            file.write(ans.response.to_text())
            print("Done.")
            file.close()
        else:
            print('Getting information..')
            ans = dns.resolver.query(domain, record)
            print(ans.response.to_text())

import dns, dns.resolver

class dns_scan(object):

    def run(self, domain, record, filename):

    def __init__(self, domain, record, filename):
        self.run(domain, record, filename)
        exit()


import dns, dns.resolver

class dns_scan(object):

    def run(self, domain, record, filename):

        if filename is not None:
            file = open(filename, 'a')
            print("File opened\n"
              "Writing the information on the file..")
            ans = dns.resolver.query(domain, record)
            file.write(ans.response.to_text())
            print("Done.")
            file.close()
        else:
            print('Getting information..')
            ans = dns.resolver.query(domain, record)
            print(ans.response.to_text())

    def __init__(self, domain, record, filename):
        self.run(domain, record, filename


"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):
        return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

"""
        Returns all unique subscribers as a set.
        """
        return {u[0] for u in get_db().fetch(self.get_query())}

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        loc_cols = ", ".join(self.spatial_unit.location_id_columns)

        sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """

        return sql


return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):

    def as_set(self):
        """
        Returns all unique subscribers as a set.
        """
        return {u[0] for u in get_db().fetch(self.get_query())}

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        loc_cols = ", ".join(self.spatial_unit.location_id_columns)

        sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """

        return sql


loc_cols = ", ".join(self.spatial_unit.location_id_columns)

"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):
        return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

    def as_set(self):
        """
        Returns all unique subscribers as a set.
        """
        return {u[0] for u in get_db().fetch(self.get_query())}

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """

        return sql


sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """

"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):
        return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

    def as_set(self):
        """
        Returns all unique subscribers as a set.
        """
        return {u[0] for u in get_db().fetch(self.get_query())}

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        loc_cols = ", ".join(self.spatial_unit.location_id_columns)


"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):
        return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

    def as_set(self):
        """
        Returns all unique subscribers as a set.
        """
        return {u[0] for u in get_db().fetch(self.get_query())}

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        loc_cols = ", ".join(self.spatial_unit.location_id_columns)

        sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """


"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standa

        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):
        return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

    def as_set(self):
        """
        Returns all unique subscribers as a set.
        """
        return {u[0] for u in get_db().fetch(self.get_query())}

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        loc_cols = ", ".join(self.spatial_unit.location_id_columns)

        sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """

        return sql


return {u[0] for u in get_db().fetch(self.get_query())}

"""
Utility classes for subsetting CDRs.

"""

from typing import List, Optional, Union, Tuple

from .direction_enum import Direction
from .event_table_subset import EventTableSubset
from .events_tables_union import EventsTablesUnion
from ...core import Query, make_spatial_unit
from ...core.context import get_db
from ...core.spatial_unit import AnySpatialUnit

from numpy import inf

import structlog

from flowmachine.utils import standardise_date

logger = structlog.get_logger("flowmachine.debug", submodule=__name__)
valid_subscriber_identifiers = ("msisdn", "imei", "imsi")

class UniqueSubscribers(Query):
    """
    Class representing the set of all unique subscribers in our interactions
    table.

    Parameters
    ----------
    start : str
        iso format date range for the beginning of the time frame,
        e.g. 2016-01-01 or 2016-01-01 14:03:01
    stop : str
        As above
    hours : tuple of ints, default None
        Subset the result within certain hours, e.g. (4,17)
        This will subset the query only with these hours, but
        across all specified days. Or set to 'all' to include
        all hours.
    table : str, default 'all'
        Table on which to perform the query. By default it will look
        at ALL tables, which are any tables with subscriber information
        in them, specified via subscriber_tables in flowmachine.yml. Otherwise
        you need to specify a full table (with a schema) such as
        'events.calls'.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Notes
    -----
    * A date without a hours and mins will be interpreted as midnight of
      that day, so to get data within a single day pass '2016-01-01',
      '2016-01-02'.

    * Use 24 hr format!

    * Will collect only onnet callers.

    Examples
    --------
    >>> UU = UniqueSubscribers('2016-01-01 13:30:30',
                         '2016-01-02 16:25:00')
    >>> UU.as_set()
    {'038OVABN11Ak4W5P',
     '09NrjaNNvDanD8pk',
     '0DB8zw67E9mZAPK2',
     ...}

    """

    def __init__(
        self,
        start: str,
        stop: str,
        *,
        hours: Optional[Tuple[int, int]] = None,
        table: Union[str, List[str]] = "all",
        subscriber_identifier: str = "msisdn",
        subscriber_subset: Optional[Query] = None,
    ):
        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.hours = hours
        self.tables = table
        self.subscriber_identifier = subscriber_identifier
        cols = [self.subscriber_identifier]
        self.unioned = EventsTablesUnion(
            self.start,
            self.stop,
            columns=cols,
            tables=self.tables,
            hours=hours,
            subscriber_subset=subscriber_subset,
            subscriber_identifier=self.subscriber_identifier,
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return self.unioned.column_names

    def _make_query(self):
        return f"SELECT unioned.subscriber FROM ({self.unioned.get_query()}) unioned GROUP BY subscriber"

    def as_set(self):
        """
        Returns all unique subscribers as a set.
        """

SubsetDates = EventTableSubset  # Backwards compatibility for unpicking queries from db

class SubscriberLocationSubset(Query):

    """
    Query to get a subset of users who have made min_calls
    number of calls within a given region during
    period of time from start to stop.

    Parameters
    ----------
    start : datetime
        Start time to filter query.
    stop : datetime
        Stop time to filter query.
    spatial_unit : flowmachine.core.spatial_unit.*SpatialUnit, default admin3
        Spatial unit to which subscriber locations will be mapped. See the
        docstring of make_spatial_unit for more information.
    min_calls : int
        minimum number of calls a user must have made within a
    direction : {'in', 'out', 'both'} or Direction, default Direction.BOTH
        Whether to consider calls made, received, or both. Defaults to 'both'.
    hours : 2-tuple of floats, default None
        Restrict the analysis to only a certain set
        of hours within each day.
    subscriber_identifier : {'msisdn', 'imei'}, default 'msisdn'
        Either msisdn, or imei, the column that identifies the subscriber.
    subscriber_subset : str, list, flowmachine.core.Query, flowmachine.core.Table, default None
        If provided, string or list of string which are msisdn or imeis to limit
        results to; or, a query or table which has a column with a name matching
        subscriber_identifier (typically, msisdn), to limit results to.

    Examples
    --------
    >>> sls = SubscriberLocationSubset("2016-01-01", "2016-01-07", min_calls=3,
        direction="both", spatial_unit=make_spatial_unit("admin", level=3))

    >>> sls.head()
          subscriber     name
    038OVABN11Ak4W5P    Dolpa
    038OVABN11Ak4W5P     Mugu
    09NrjaNNvDanD8pk    Banke
    0ayZGYEQrqYlKw6g    Dolpa
    0DB8zw67E9mZAPK2  Baglung
    ...
    """

    def __init__(
        self,
        start,
        stop,
        *,
        min_calls,
        subscriber_identifier="msisdn",
        direction: Union[str, Direction] = Direction.BOTH,
        spatial_unit: Optional[AnySpatialUnit] = None,
        hours: Optional[Tuple[int, int]] = None,
        subscriber_subset=None,
    ):

        from ...features import PerLocationSubscriberCallDurations

        self.start = standardise_date(start)
        self.stop = standardise_date(stop)
        self.min_calls = min_calls
        self.subscriber_identifier = subscriber_identifier
        self.direction = Direction(direction)
        if spatial_unit is None:
            self.spatial_unit = make_spatial_unit("admin", level=3)
        else:
            self.spatial_unit = spatial_unit

        self.pslds = PerLocationSubscriberCallDurations(
            start=self.start,
            stop=self.stop,
            subscriber_identifier=self.subscriber_identifier,
            direction=self.direction,
            spatial_unit=self.spatial_unit,
            statistic="count",
            hours=hours,
            subscriber_subset=subscriber_subset,
        )

        self.pslds_subset = self.pslds.numeric_subset(
            "value", low=self.min_calls, high=inf
        )

        super().__init__()

    @property
    def column_names(self) -> List[str]:
        return ["subscriber"] + self.spatial_unit.location_id_columns

    def _make_query(self):

        loc_cols = ", ".join(self.spatial_unit.location_id_columns)

        sql = f"""
        SELECT
            subscriber,
            {loc_cols}
        FROM
            ({self.pslds_subset.get_query()}) _
        """

        return sql


for line in f:
        tokens = line.strip().replace('\"', '').split(' ')
        author_id_to_name[int(tokens[0])] = tokens[1]

import ranklist
from plotSpot import plotSpot
from plot_functions import *
import cPickle as pickle
from sklearn.ensemble import IsolationForest
from matplotlib.backends.backend_pdf import PdfPages

num_outlier = 10
budget = 4

outlier_file_name = "top10-sample_best"  # my personal best, containing Christos' name

features = []
if "best" in outlier_file_name:
    features = ['DEST', 'EDGES_OUT', 'LIFE', 'MEAN_IAT', 'IAT_VAR_MEAN',
                'IAT_q50']
else:
    features = ['DEST', 'EDGES_OUT', 'LIFE',
                'MEAN_IAT', 'IAT_VAR_MEAN', 'IAT_MIN', 'IAT_MAX',
                ] + ["IAT_q%d" % q for q in range(10, 91, 10)]

print 'loading'
INFO = pickle.load(open('results/info.pkl', 'rb'))
outliers = pickle.load(open('results/%s.pkl' % outlier_file_name, 'rb'))[:num_outlier]
global_outlier_list = outliers

print 'outlier id to name mapping'
author_id_to_name = {}
with open('Data/ent.dblp_coauthor') as f:

# print outliers

outlier_ids = [INFO['IDs'].index(i) for i in outliers]

# score from pair plots
feature_pairs = generate_pairs(features, features)
print feature_pairs
rank_matrix = []

pp = PdfPages(plotfolder + 'sample-scatterplots.pdf')
for j, features in enumerate(feature_pairs):
    X, Y = features[0], features[1]
    print j, 'of', len(feature_pairs)
    pair_features = np.array([INFO[features[0]], INFO[features[1]]]).T
    forest = IsolationForest(
        n_estimators=100, #max_samples=1000,
        random_state=0, contamination=num_outlier / float(len(INFO['IDs'])) # number of nodes
    )
    fig = scatter_plot(INFO[X], INFO[Y], INFO['IDs'], discription[Y],
                                  discription[X],
                                  discription[Y] + ' vs ' + discription[X],
                                  compare_value[X])
    forest.fit(pair_features)
    scores = forest.decision_function(pair_features[outlier_ids, :])
    rank_list = sorted([(outliers[i], -s) for (i, s) in enumerate(scores)],
                       key=lambda x: x[1], reverse=True)
    rank_matrix.append(rank_list)
pp.close()

print rank_matrix

#  runs, properly till this, why is generate_graph returning nothing?
scaled_matrix, normal_matrix = ranklist.generate_graph(P_val, num_outlier, rank_matrix)
plots = plotSpot(budget, scaled_matrix, "SpellOut")
frequencies = generate_frequency_list(plots, scaled_matrix)
for i, plot in enumerate(plots):
    fig = scatter_outliers(plot, INFO['IDs'], frequencies)
    fname = 'discoveries/DBLP-sample-{0}-{1}-{2}.png'.format(num_outlier, budget, i)
    fig.savefig(fname)


import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:
        if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

"""
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):
        bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0
        string = rf"\ottava {n}"
        if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """
        return self._format_slot

    @property
    def n(self) -> typing.Optional[int]:
        """
        Gets octave change.
        """
        return self._n


if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

    def __repr__(self) -> str:
        """
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):
        bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0
        string = rf"\ottava {n}"
        if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """
        return self._format_slot

    @property
    def n(self) -> typing.Optional[int]:
        """
        Gets octave change.
        """
        return self._n


bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0

import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:
        if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

    def __repr__(self) -> str:
        """
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):

        string = rf"\ottava {n}"
        if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """
        return self._format_slot

    @property
    def n(self) -> typing.Optional[int]:
        """
        Gets octave change.
        """
        return self._n


import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:
        if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

    def __repr__(self) -> str:
        """
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):
        bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0

        if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """
        return self._format_slot

    @property
    def n(self) -> typing.Optional[int]:
        """
        Gets octave change.
        """
        return self._n


if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:
        if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

    def __repr__(self) -> str:
        """
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):
        bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0
        string = rf"\ottava {n}"

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """
        return self._format_slot

    @property
    def n(self) -> typing.Optional[int]:
        """
        Gets octave change.
        """
        return self._n


import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:
        if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

    def __repr__(self) -> str:
        """
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):
        bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0
        string = rf"\ottava {n}"
        if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """
        ret

    @property
    def n(self) -> typing.Optional[int]:
        """
        Gets octave change.
        """
        return self._n


import typing

from .. import bundle as _bundle
from .. import format as _format

class Ottava:
    r"""
    LilyPond ``\ottava`` command.

    ..  container:: example

        >>> staff = abjad.Staff("c'4 d' e' f'")
        >>> ottava = abjad.Ottava(n=1)
        >>> abjad.attach(ottava, staff[0])
        >>> ottava = abjad.Ottava(n=0, format_slot='after')
        >>> abjad.attach(ottava, staff[-1])
        >>> abjad.show(staff) # doctest: +SKIP

        ..  docs::

            >>> string = abjad.lilypond(staff)
            >>> print(string)
            \new Staff
            {
                \ottava 1
                c'4
                d'4
                e'4
                f'4
                \ottava 0
            }

    """

    __slots__ = ("_format_slot", "_n")

    persistent = True

    def __init__(self, n: int = None, *, format_slot: str = None) -> None:
        if n is not None:
            assert isinstance(n, int), repr(n)
        self._n = n
        if format_slot is not None:
            assert format_slot in ("before", "after"), repr(format_slot)
        self._format_slot = format_slot

    def __eq__(self, argument) -> bool:
        """
        Delegates to ``abjad.format.compare_objects()``.
        """
        return _format.compare_objects(self, argument)

    def __hash__(self) -> int:
        """
        Hashes ottava.
        """
        return hash(self.__class__.__name__ + str(self))

    def __repr__(self) -> str:
        """
        Delegates to storage format manager.
        """
        return _format.get_repr(self)

    def _get_lilypond_format_bundle(self, component=None):
        bundle = _bundle.LilyPondFormatBundle()
        n = self.n or 0
        string = rf"\ottava {n}"
        if self.format_slot in ("before", None):
            bundle.before.commands.append(string)
        else:
            assert self.format_slot == "after"
            bundle.after.commands.append(string)
        return bundle

    @property
    def format_slot(self) -> typing.Optional[str]:
        r"""
        Gets format slot.

        ..  container:: example

            Format slot defaults to before:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0)
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    \ottava 0
                    f'4
                }

            Set format slot to after like this:

            >>> staff = abjad.Staff("c'4 d' e' f'")
            >>> ottava = abjad.Ottava(n=1)
            >>> abjad.attach(ottava, staff[0])
            >>> ottava = abjad.Ottava(n=0, format_slot='after')
            >>> abjad.attach(ottava, staff[-1])
            >>> abjad.show(staff) # doctest: +SKIP

            ..  docs::

                >>> string = abjad.lilypond(staff)
                >>> print(string)
                \new Staff
                {
                    \ottava 1
                    c'4
                    d'4
                    e'4
                    f'4
                    \ottava 0
                }

        """