def graph(config, user_id, depth, max_follow, username_file, graph_file):
    '''
    Get follower/following graph.

    Starting with the seed user identified by USER_ID, construct a directed
    follower/following graph by traversing up to --depth hops (default: 1).
    For each user, fetch only --max-follow followers and followees.
    '''

    # Get username for this user_id.
    endpoint = '/users/{}'.format(user_id)
    response = _get_instagram(config, endpoint)

    if response.status_code != 200:
        raise click.ClickException(
            'Unable to fetch user information: {} {}'
            .format(response.status_code, response.payload['meta']['error_message'])
        )

    user_info = json.loads(response.text)
    username = user_info['data']['username']

    # Get graph.
    node_fn = functools.partial(_get_graph, config, max_follow)
    users, graph = get_graph(node_fn, {user_id: username}, depth)

    write_users(users, username_file)
    write_graph(graph, graph_file)

    click.secho('Finished: {} nodes'.format(len(users)))
Example #2
0
    def test_get_graph_depth_1_5(self):
        ''' Test graph generation at depth 1.5. '''

        node_fn = functools.partial(self._generate_node)
        users, graph = util.get_graph(node_fn, seeds={'1': 'user1'}, max_depth=1.5)

        # 1 seed node plus 4 adjacent nodes
        self.assertEqual(5, len(users))

        # All edges from test_get_graph_depth_1() are also present here, plus
        # one additional edge from 10 to 12.
        self.assertEqual(5, self._count_edges(graph))

        # Node 1 has 2 friends.
        self.assertEqual({'10', '11'}, graph['1'])

        # Node 1 has 2 followers.
        self.assertIn('1', graph['12'])
        self.assertIn('1', graph['13'])

        # Node 10 is in the user list, but node 100 is not (because we don't
        # include nodes from 2 hops).
        self.assertIn('10', users)
        self.assertNotIn('100', users)

        # Node 10 follows node 11 -- this is the key difference between this
        # test and test_get_graph_depth_1().
        self.assertIn('11', graph['10'])
Example #3
0
    def test_get_graph_depth_2_5(self):
        ''' Test graph generation at depth 2.5. '''

        node_fn = functools.partial(self._generate_node)
        users, graph = util.get_graph(node_fn, seeds={'1': 'user1'}, max_depth=2.5)

        # 21 nodes: 1 seed node, 4 nodes within one hop, 16 nodes within two
        # hops.
        self.assertEqual(21, len(users))

        # Same edges as test_get_graph_depth_2() plus 4 extra edges (100->101,
        # 110->111, 120->121, 130->131).
        self.assertEqual(25, self._count_edges(graph))

        # Node 10 has 3 friends.
        self.assertEqual({'11', '100', '101'}, graph['10'])

        # Node 10 has 3 followers.
        self.assertIn('10', graph['1'])
        self.assertIn('10', graph['102'])
        self.assertIn('10', graph['103'])

        # Node 100 follows 101. This is the main difference between this test
        # and test_get_graph_depth_2().
        self.assertIn('101', graph['100'])
Example #4
0
def graph(config, depth, max_follow, username, username_file, graph_file):
    ''' Get a twitter user's friends/follower graph. '''

    session = _login_twitter(config)

    # Get user ID.
    home_url = '{}/{}'.format(config.twitter_url, username)
    response = session.get(home_url)

    if response.status_code != 200:
        raise click.ClickException('Not able to get home page for {}. ({})'
                                   .format(username, response.status_code))

    html = bs4.BeautifulSoup(response.text, 'html.parser')
    profile_el = html.select('.ProfileNav-item--userActions .user-actions')[0]
    user_id = profile_el['data-user-id']

    # Get graph.
    node_fn = functools.partial(_get_graph, session, config, max_follow)
    users, graph = get_graph(node_fn, {user_id: username}, depth)

    write_users(users, username_file)
    write_graph(graph, graph_file)

    click.secho('Finished: {} nodes'.format(len(users)))
Example #5
0
    def test_get_graph_depth_1(self):
        ''' Test graph generation at depth 1. '''

        node_fn = functools.partial(self._generate_node)
        users, graph = util.get_graph(node_fn, seeds={'1': 'user1'}, max_depth=1)

        # 1 seed node plus 4 adjacent nodes
        self.assertEqual(5, len(users))

        # One edge between the seed and each of its 4 neighbors.
        self.assertEqual(4, self._count_edges(graph))

        # Node 1 has 2 friends.
        self.assertEqual({'10', '11'}, graph['1'])

        # Node 1 has 2 followers.
        self.assertIn('1', graph['12'])
        self.assertIn('1', graph['13'])

        # Even though node 10 follows node 11, that edge should not be part
        # of a depth 1 graph. (It is included in depth 1.5: see below.)
        self.assertNotIn('11', graph['10'])
Example #6
0
    def test_get_graph_depth_2(self):
        ''' Test graph generation at depth 2. '''

        node_fn = functools.partial(self._generate_node)
        users, graph = util.get_graph(node_fn, seeds={'1': 'user1'}, max_depth=2)

        # 21 nodes: 1 seed node, 4 nodes within one hop, 16 nodes within two
        # hops.
        self.assertEqual(21, len(users))

        # 5 edges in the first hop + 4 edges * 4 nodes in the second hop.
        self.assertEqual(21, self._count_edges(graph))

        # Node 10 has 3 friends.
        self.assertEqual({'11', '100', '101'}, graph['10'])

        # Node 10 has 3 followers.
        self.assertIn('10', graph['1'])
        self.assertIn('10', graph['102'])
        self.assertIn('10', graph['103'])

        # Node 100 follows 101, but that edge should not appear in this graph.
        # It should appear in the depth=2.5 graph (see below).
        self.assertNotIn('101', graph['100'])