<file_sep>from unittest import TestCase
import io
import json
from contextlib import redirect_stdout

import requests
import requests_mock

from ..fetch_facebook_posts_data import (fetch_node_feed, fetch_posts,
                                         format_datetime_string,
                                         clean_reactions_data)
from .constants import (
    BASE_FB_URL, REACTIONS_URL, FACEBOOK_POSTS_ONE_RECORD_RAW_DATA,
    FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA,
    SAMPLE_COUNT, FACEBOOK_LAST_PAGE_RECORD_RAW_DATA,
    FACEBOOK_LAST_PAGE_RECORD_CLEAN_DATA,
    FACEBOOK_POSTS_MULT_RECORD_RAW_DATA,
    FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA)


AUTH_TOKEN_INVALID_DATA = {
    "error": {
        "message": "Invalid OAuth access token.",
        "type": "OAuthException",
        "code": 190,
        "fbtrace_id": "ADSG8pUqsRg"
    }
}


def read_access_token():
    with open('config.json') as config_file:
        data = json.load(config_file)
    return data['access_token']


def equal_dicts(d1, d2, ignore_keys=None):
    if isinstance(ignore_keys, str):
        ignore_keys = [ignore_keys, ]
    ignored = set(ignore_keys)
    for k1, v1 in d1.items():
        if k1 not in ignored and (k1 not in d2 or d2[k1] != v1):
            return False
    for k2, _ in d2.items():
        if k2 not in ignored and k2 not in d1:
            return False
    return True


def sysoutput_to_dicts(out):
    return [json.loads(x) for x in out.getvalue().split('\n')[:-1]]


class TestFetchFacebookPostsData(TestCase):

    def test_raise_error_for_non_200_response(self):
        error_message = (
            b'{"error":{"message":"Error validating access token:'
            b' Session has expired on Saturday, 24-Mar-18 05:00:00 PDT. '
            b'The current time is Saturday, 24-Mar-18 05:20:17 PDT.",'
            b'"type":"OAuthException","code":190,"error_subcode":463,"'
            b'fbtrace_id":"ACz1b5AhZtt"}}')
        with requests_mock.Mocker() as m:
            request_url = BASE_FB_URL + 'account_id' + REACTIONS_URL + 'AAA'
            m.register_uri(
                'GET',
                request_url,
                status_code=400,
                content=error_message)
            with self.assertRaises(requests.exceptions.HTTPError) as error_manager:
                fetch_node_feed('account_id', access_token='AAA')

            # It's given us a helpful exception
            self.assertIn(
                '400 Client Error: {"error":{"message":"Error validating access token',
                error_manager.exception.__str__())

    def test_dont_raise_error_for_200_response(self):
        with requests_mock.Mocker() as m:
            request_url = BASE_FB_URL + 'account_id' + REACTIONS_URL + 'AAA'
            m.register_uri(
                'GET', request_url, status_code=200, content=json.dumps(
                    FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))
            response = fetch_node_feed('account_id', access_token='AAA')

            self.assertEqual(FACEBOOK_POSTS_ONE_RECORD_RAW_DATA, response)

    def test_can_write_schema(self):
        out = io.StringIO()
        with redirect_stdout(out):
            with requests_mock.Mocker() as m:
                request_url = BASE_FB_URL + 'account_id' + REACTIONS_URL + 'AAA'
                m.register_uri(
                    'GET', request_url, status_code=200, content=json.dumps(
                        FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))
                fetch_posts(node_id='account_id', access_token='AAA')

        out_dicts = sysoutput_to_dicts(out)
        row_types = [x['type'] for x in out_dicts]
        # We have a schema record type
        self.assertIn('SCHEMA', row_types)

    def test_can_write_one_record(self):
        out = io.StringIO()
        with requests_mock.Mocker() as m:
            request_url = BASE_FB_URL + 'officialstackoverflow' + REACTIONS_URL + 'AAA'
            m.register_uri('GET', request_url, status_code=200, content=json.dumps(
                FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))

            with redirect_stdout(out):
                fetch_posts('officialstackoverflow', 'AAA')

        out_dicts = sysoutput_to_dicts(out)
        row_types = [x['type'] for x in out_dicts]
        # We wrote at least 1 record
        self.assertIn('RECORD', row_types)
        # And it contains our expected data
        self.assertTrue(
            equal_dicts(
                out_dicts[1]['record'],
                FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA['data'][0],
                ignore_keys='created_time'))

    def test_can_write_multiple_records(self):
        out = io.StringIO()
        with requests_mock.Mocker() as m:
            request_url = BASE_FB_URL + 'officialstackoverflow' + REACTIONS_URL + 'AAA'
            m.register_uri('GET', request_url, status_code=200, content=json.dumps(
                           FACEBOOK_POSTS_MULT_RECORD_RAW_DATA).encode('utf-8'))

            with redirect_stdout(out):
                fetch_posts('officialstackoverflow', 'AAA')

        out_dicts = sysoutput_to_dicts(out)
        row_types = [x['type'] for x in out_dicts]
        # We wrote at least 1 record
        self.assertIn('RECORD', row_types)
        # And it contains both of our expected data points
        self.assertTrue(
            equal_dicts(
                out_dicts[1]['record'],
                FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA['data'][0],
                ignore_keys='created_time'))
        self.assertTrue(
            equal_dicts(
                out_dicts[2]['record'],
                FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA['data'][1],
                ignore_keys='created_time'))

    def test_records_have_singer_format_string_times(self):
        out = io.StringIO()
        with requests_mock.Mocker() as m:
            request_url = BASE_FB_URL + 'officialstackoverflow' + REACTIONS_URL + 'AAA'
            m.register_uri('GET', request_url, status_code=200, content=json.dumps(
                FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))

            with redirect_stdout(out):
                fetch_posts('officialstackoverflow', 'AAA')

        out_dicts = sysoutput_to_dicts(out)

        # It has been converted
        self.assertNotEqual(
            out_dicts[1]['record']['created_time'],
            FACEBOOK_POSTS_ONE_RECORD_RAW_DATA['data'][0]['created_time'])
        # to the format that Singer expects
        self.assertEqual(out_dicts[1]['record']['created_time'],
                         '2018-03-19T18:17:00Z')

    def test_format_datetime_string_correctly(self):
        fb_format_datetime = '2018-03-19T18:17:00+0000'
        formatted_datetime = format_datetime_string(fb_format_datetime)

        self.assertEqual(formatted_datetime, '2018-03-19T18:17:00Z')

    def test_invalid_config_access_token(self):
        out = io.StringIO()
        with requests_mock.Mocker() as m:
            request_url = (BASE_FB_URL +
                           'officialstackoverflow/feed?access_token=AAA')
            m.register_uri(
                'GET',
                request_url,
                status_code=200,
                content=json.dumps(AUTH_TOKEN_INVALID_DATA).encode('utf-8'))

        with redirect_stdout(out):
            with self.assertRaises(requests.exceptions.HTTPError) as error_manager:
                fetch_posts('officialstackoverflow', 'AAA')

        error_message = error_manager.exception.args[0]
        self.assertIn('400 Client Error', error_message)
        self.assertIn('Invalid OAuth access token', error_message)


class TestCanFetchPostStats(TestCase):

    def test_can_get_reaction_count_from_original_api_call(self):
        with requests_mock.Mocker() as m:
            reactions_url = BASE_FB_URL + 'account_id' + REACTIONS_URL + 'AAA'
            m.register_uri(
                'GET', reactions_url, status_code=200, content=json.dumps(
                    FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))

            response = fetch_node_feed('account_id', access_token='AAA')

            reaction_labels = [
                'none_reaction_count',
                'sad_count',
                'like_count',
                'love_count',
                'pride_count',
                'total_reaction_count',
                'haha_count',
                'wow_count',
                'thankful_count',
                'angry_count']
            for r in reaction_labels:
                self.assertTrue(response['data'][0][r])

    def test_can_get_clean_reaction_count(self):
        reaction_labels = [
            'none_reaction_count', 'sad_count', 'like_count', 'love_count',
            'pride_count', 'total_reaction_count', 'haha_count', 'wow_count',
            'thankful_count', 'angry_count'
        ]
        reactions_data = {}
        for reaction in reaction_labels:
            reactions_data[reaction] = SAMPLE_COUNT

        cleaned_data = clean_reactions_data(reactions_data)

        # And our resulting data is flat
        self.assertEqual(cleaned_data['none_reaction_count'], 566)
        self.assertEqual(cleaned_data['sad_count'], 566)

    def test_can_get_clean_comment_count(self):
        reactions_data = {
            "comment_count": {
                "data": [
                ],
                "summary": {
                    "order": "ranked",
                    "total_count": 7,
                    "can_comment": True
                }
            }
        }

        cleaned_data = clean_reactions_data(reactions_data)

        # And our resulting data is flat
        self.assertEqual(cleaned_data['comment_count'], 7)


class TestStateAndPagination(TestCase):

    def test_can_write_state(self):
        out = io.StringIO()
        with redirect_stdout(out):
            with requests_mock.Mocker() as m:
                request_url = BASE_FB_URL + 'account_id' + REACTIONS_URL + 'AAA'
                m.register_uri('GET', request_url, status_code=200, content=json.dumps(
                    FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))
                fetch_posts(node_id='account_id', access_token='AAA')

        out_dicts = sysoutput_to_dicts(out)
        row_types = [x['type'] for x in out_dicts]
        # We have a state record type
        self.assertIn('STATE', row_types)

    def test_can_start_pulling_data_at_a_certain_page(self):
        out = io.StringIO()
        with requests_mock.Mocker() as m:
            # Our first request URL points at second_request_uri
            self.assertEqual(
                FACEBOOK_POSTS_ONE_RECORD_RAW_DATA['paging']['cursors']['after'],
                'after_cursor_ZZZ')
            first_request_url = (
                BASE_FB_URL +
                'account_id' +
                REACTIONS_URL +
                'AAA'
                '&after=after_cursor_YYY')
            m.register_uri(
                'GET', first_request_url, status_code=200, content=json.dumps(
                    FACEBOOK_POSTS_ONE_RECORD_RAW_DATA).encode('utf-8'))

            second_request_url = (
                BASE_FB_URL +
                'account_id' +
                REACTIONS_URL +
                'AAA'
                '&after=after_cursor_ZZZ')
            # Our second_request_uri does not have an after cursor marker
            m.register_uri(
                'GET', second_request_url, status_code=200, content=json.dumps(
                    FACEBOOK_LAST_PAGE_RECORD_RAW_DATA).encode('utf-8'))
            with self.assertRaises(KeyError):
                FACEBOOK_LAST_PAGE_RECORD_RAW_DATA['paging']['cursors']['after']

            # Fetch posts, with a cursor that points at our first URL
            with redirect_stdout(out):
                fetch_posts(node_id='account_id', access_token='AAA',
                            after_state_marker='after_cursor_YYY')

            out_dicts = sysoutput_to_dicts(out)
            # It recorded the state as expected
            row_messages = [x['record']['message']
                            for x in out_dicts if x['type'] == 'RECORD']
            state_record = [x for x in out_dicts if x['type'] == 'STATE'][0]
            # It fetched both of our messages
            self.assertIn(
                FACEBOOK_LAST_PAGE_RECORD_CLEAN_DATA['data'][0]['message'],
                row_messages)
            self.assertIn(
                FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA['data'][0]['message'],
                row_messages)
            # And recorded the state of the first call
            self.assertEqual(
                state_record['value']['after'],
                FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA['paging']['cursors']['after'])
<file_sep>from setuptools import setup, find_packages

setup(name="tap-facebook-posts",
      version="0.0.5",
      description="Singer.io tap for extracting posts data from the Facebook Graph API",
      author="rowanv",
      license='Apache 2.0',
      url="https://github.com/rowanv",
      classifiers=["Programming Language :: Python :: 3 :: Only"],
      py_modules=["tap_facebook_posts"],
      install_requires=[
        'python-dateutil==2.6.0',  # This is required by singer-python,
          # without this being here explicitly, there are dependency issues
        'requests',
        'singer-python==5.0.12',
      ],
      python_requires='>=3',
      entry_points="""
          [console_scripts]
          tap-facebook-posts=tap_facebook_posts:main
      """,
      packages=["tap_facebook_posts"],
      package_data = {
          "schemas": ["tap_facebook_posts/schemas/*.json"]
      },
      include_package_data=True,
)
<file_sep>
BASE_FB_URL = 'https://graph.facebook.com/v2.11/'
REACTIONS_URL = (
    '/posts?fields=created_time,story,message,'
    'reactions.limit(0).summary(1).as(total_reaction_count),'
    'reactions.type(NONE).limit(0).summary(1).as(none_reaction_count),'
    'reactions.type(LIKE).limit(0).summary(1).as(like_count),'
    'reactions.type(LOVE).limit(0).summary(1).as(love_count),'
    'reactions.type(HAHA).limit(0).summary(1).as(haha_count),'
    'reactions.type(WOW).limit(0).summary(1).as(wow_count),'
    'reactions.type(SAD).limit(0).summary(1).as(sad_count),'
    'reactions.type(ANGRY).limit(0).summary(1).as(angry_count),'
    'reactions.type(THANKFUL).limit(0).summary(1).as(thankful_count),'
    'reactions.type(PRIDE).limit(0).summary(1).as(pride_count),'
    'comments.limit(0).summary(1).as(comment_count)'
    '&limit=100&access_token='
)


# Create the test values that the API must return. We dynamically create
# the reactions variables for both a RAW_DATA variable that simulates the
# raw data returned by the API, and a CLEAN_DATA variable that simulates
# the flattened data that our module returns


SAMPLE_COUNT = {
    "data": [
    ],
    "summary": {
        "total_count": 566,
        "viewer_reaction": "NONE"
    }
}


FACEBOOK_POSTS_ONE_RECORD_DATA = {
    'data': [{
        'created_time': '2018-03-19T18:17:00+0000',
        'message': 'This is a test message',
        'id': '11239244970_10150962953494971',
        "comment_count": {
            "data": [
            ],
            "summary": {
                "order": "ranked",
                "total_count": 7,
                "can_comment": True
            }
        },
    }, ],
    'paging': {
        'cursors': {'before': 'before_cursor_AAA', 'after': 'after_cursor_ZZZ'},
        'next': 'https://graph.facebook.com/v2.11/11239244970/feed?access_token=AAA'}
}

# Add in the reactions
REACTION_LABELS = [
    'none_reaction_count', 'sad_count', 'like_count', 'love_count',
    'pride_count', 'total_reaction_count', 'haha_count', 'wow_count',
    'thankful_count', 'angry_count'
]

FACEBOOK_POSTS_ONE_RECORD_RAW_DATA = dict(FACEBOOK_POSTS_ONE_RECORD_DATA)
FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA = dict(FACEBOOK_POSTS_ONE_RECORD_DATA)

for reaction in REACTION_LABELS:

    FACEBOOK_POSTS_ONE_RECORD_RAW_DATA['data'][0][reaction] = SAMPLE_COUNT
    FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA['data'][0][reaction] = 400
FACEBOOK_POSTS_ONE_RECORD_CLEAN_DATA['data'][0]['comment_count'] = 7

FACEBOOK_LAST_PAGE_RECORD_DATA = {
    'data': [{
        'created_time': '2018-03-19T18:17:00+0000',
        'message': 'This is a the last page record message',
        'id': '11239244970_10150962953494971'}, ],
    "comment_count": {
        "data": [
        ],
        "summary": {
            "order": "ranked",
            "total_count": 7,
            "can_comment": True
        }
    },
    'paging': {
        'cursors': {'before': 'before_cursor_BBB'}, }
}

FACEBOOK_LAST_PAGE_RECORD_CLEAN_DATA = FACEBOOK_LAST_PAGE_RECORD_DATA.copy()
FACEBOOK_LAST_PAGE_RECORD_RAW_DATA = FACEBOOK_LAST_PAGE_RECORD_DATA.copy()

for reaction in REACTION_LABELS:
    FACEBOOK_LAST_PAGE_RECORD_CLEAN_DATA['data'][0][reaction] = 400
    FACEBOOK_LAST_PAGE_RECORD_RAW_DATA['data'][0][reaction] = SAMPLE_COUNT
FACEBOOK_LAST_PAGE_RECORD_CLEAN_DATA['data'][0]['comment_count'] = 7


FACEBOOK_POSTS_MULT_RECORD_DATA = {
    'data': [{
        'created_time': '2018-03-19T18:17:00+0000',
        'message': 'This is a test message',
        'id': '11239244970_10150962953494971',
    }, {
        'created_time': '2018-03-19T18:17:00+0000',
        'message': 'This is a anothertest message',
        'id': '11239244970_10150962953494111',
        "comment_count": {
            "data": [
            ],
            "summary": {
                "order": "ranked",
                "total_count": 7,
                "can_comment": True
            }
        },
    }],
    'paging': {
        'cursors': {'before': 'before_cursor_AAA', 'after': 'after_cursor_ZZZ'},
        'next': BASE_FB_URL + '11239244970' + REACTIONS_URL + 'AAA'}
}

FACEBOOK_POSTS_MULT_RECORD_RAW_DATA = FACEBOOK_POSTS_MULT_RECORD_DATA.copy()
FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA = FACEBOOK_POSTS_MULT_RECORD_DATA.copy()

for reaction in REACTION_LABELS:
    FACEBOOK_POSTS_MULT_RECORD_RAW_DATA['data'][0][reaction] = SAMPLE_COUNT
    FACEBOOK_POSTS_MULT_RECORD_RAW_DATA['data'][1][reaction] = SAMPLE_COUNT
    FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA['data'][0][reaction] = 400
    FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA['data'][1][reaction] = 400
FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA['data'][0]['comment_count'] = 7
FACEBOOK_POSTS_MULT_RECORD_CLEAN_DATA['data'][1]['comment_count'] = 7
<file_sep>autopep8==1.3.4
flake8==3.5.0
nose==1.3.7
singer-tools==0.4.1
requests-mock==1.4.0
<file_sep>#!/bin/bash

# Check that source has correct formatting
autopep8 tap_facebook_posts/*/*.py -r --diff

# More comprehensive static analysis, complexity check
flake8 tap_facebook_posts/<file_sep>import argparse
import json

import singer

from .fetch_facebook_posts_data import fetch_posts


logger = singer.get_logger()


def create_parser():
    parser = argparse.ArgumentParser()

    parser.add_argument(
        '-c', '--config', help='Config file', required=True)
    parser.add_argument(
        '-s', '--state', help='State file')
    return parser


def main():
    parser = create_parser()
    args = parser.parse_args()

    with open(args.config) as config_file:
        config = json.load(config_file)
    required_config_vars = ['access_token', 'node_id', ]
    for r in required_config_vars:
        if r not in config:
            logger.fatal("Missing required configuration keys: {}".format(r))
            exit(1)

    after_state_marker = None
    if args.state:
        with open(args.state) as state_file:
            state = json.load(state_file)
            after_state_marker = state['after']

    try:
        fetch_posts(config['node_id'], access_token=config['access_token'],
                    after_state_marker=after_state_marker)
    except Exception as exception:
        logger.critical(exception)
        raise exception


if __name__ == "__main__":
    main()
<file_sep>from unittest import TestCase

from .. import create_parser


class TestTapValid(TestCase):

    def setUp(self):
        self.parser = create_parser()

    def test_can_call_tap_through_command_line(self):
        parsed = self.parser.parse_args(['--config', 'config.json'])
        self.assertTrue(parsed)
<file_sep>python-dateutil==2.6.0
requests==2.18.4
singer-python==5.0.15
<file_sep>import os
import json
import datetime

import requests
import singer


MAX_REQUEST_ITERATIONS = 50
BASE_FACEBOOK_GRAPH_API_URL = 'https://graph.facebook.com/v2.11/'
REACTIONS_URL = (
    '/posts?fields=created_time,story,message,'
    'reactions.limit(0).summary(1).as(total_reaction_count),'
    'reactions.type(NONE).limit(0).summary(1).as(none_reaction_count),'
    'reactions.type(LIKE).limit(0).summary(1).as(like_count),'
    'reactions.type(LOVE).limit(0).summary(1).as(love_count),'
    'reactions.type(HAHA).limit(0).summary(1).as(haha_count),'
    'reactions.type(WOW).limit(0).summary(1).as(wow_count),'
    'reactions.type(SAD).limit(0).summary(1).as(sad_count),'
    'reactions.type(ANGRY).limit(0).summary(1).as(angry_count),'
    'reactions.type(THANKFUL).limit(0).summary(1).as(thankful_count),'
    'reactions.type(PRIDE).limit(0).summary(1).as(pride_count),'
    'comments.limit(0).summary(1).as(comment_count)'
    '&limit=100&access_token='
)


def get_abs_path(path):
    return os.path.join(os.path.dirname(os.path.realpath(__file__)), path)


def load_schema(entity):
    return singer.utils.load_json(get_abs_path(
                                  "schemas/{}.json".format(entity)))


def format_datetime_string(original_dt):
    """Convert datetime into formatted string that is compatible w/ Singer spec.
    """
    FB_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S+%f"
    TARGET_DATETIME_PARSE = "%Y-%m-%dT%H:%M:%SZ"
    dt = datetime.datetime.strptime(original_dt, FB_DATETIME_FORMAT)
    return dt.strftime(TARGET_DATETIME_PARSE)


def fetch_node_feed(node_id, access_token, *, after_state_marker=None):
    """Fetch the feed with all Post nodes for a given node.

    For more info, see
        https://developers.facebook.com/docs/graph-api/using-graph-api/

    node_id: str or int, unique identifier for a given node.
    access_token: str, access token to Facebook Graph API
    after_state_marker: If provided, will only fetch posts after that marker.
    """
    base_url = (BASE_FACEBOOK_GRAPH_API_URL + str(node_id) +
                REACTIONS_URL + access_token)
    if not after_state_marker:
        url = base_url
    else:
        url = base_url + '&after=' + after_state_marker
    response = requests.get(url)
    if response.status_code == 200:
        return json.loads(response.content.decode('utf-8'))
    else:
        error_message = (str(response.status_code) + ' Client Error: ' +
                         response.content.decode('utf-8'))
        raise requests.exceptions.HTTPError(error_message)


def clean_reactions_data(record):
    # Flatten the reactions data
    reactions = [
        'none_reaction_count', 'sad_count', 'like_count', 'love_count',
        'pride_count', 'total_reaction_count', 'haha_count', 'wow_count',
        'thankful_count', 'angry_count', 'comment_count',
    ]
    for key, value in record.items():
        if key in reactions:
            try:
                record[key] = record[key]['summary']['total_count']
            except TypeError:
                pass  # record is already clean
    return record


def write_records(data):
    for record in data:
        record['created_time'] = format_datetime_string(record['created_time'])
        record = clean_reactions_data(record)
        singer.write_record('facebook_posts', record)


def fetch_posts(node_id, access_token, after_state_marker=None):
    """Fetches all posts for a given node.

    node_id:  str or int, unique identifier for a given node.
    access_token: str, access token to Facebook Graph API
    after_state_marker: If provided, will only fetch posts after that marker.
    """
    current_state_marker = after_state_marker
    request_iterations_count = MAX_REQUEST_ITERATIONS
    schema = load_schema("facebook_posts")
    singer.write_schema("facebook_posts", schema, key_properties=["id"])

    node_feed = fetch_node_feed(node_id, access_token, after_state_marker=after_state_marker)
    write_records(node_feed['data'])
    try:
        while node_feed['paging']['cursors']['after'] and request_iterations_count:
            response_after_state_marker = node_feed['paging']['cursors']['after']
            # Did we already fetch the reponse marked by this state marker?
            if current_state_marker != response_after_state_marker:
                singer.write_state({'after': response_after_state_marker})
                node_feed = fetch_node_feed(
                    node_id, access_token,
                    after_state_marker=response_after_state_marker)
                write_records(node_feed['data'])
                request_iterations_count -= 1
            else:
                break
    except KeyError:
        pass
<file_sep># tap-facebook-posts
Singer.io tap for getting Facebook posts from the Facebook Graph API.


[![PyPI version](https://badge.fury.io/py/tap-facebook-posts.svg)](https://badge.fury.io/py/tap-facebook-posts)



This tap:
- Pulls posts data for a given node from the [Facebook Graph API]. If no state is specified, it will pull all
available data for the feed, up to a limit of 50 API calls.
- Outputs the schema for the posts resource
- Prints the 'after' cursor (the next post that one should request from the API) to STATE
- Can fetch all posts after an 'after' cursor if provided with a `-s` flag

[Facebook Graph API]: https://developers.facebook.com/docs/graph-api

## Connecting tap-facebook-posts

### Quick Start

1. Install

We recommend using a virtualenv
```
virtualenv -p python3 my_venv
source my_venv/bin/activate
pip install tap-facebook-posts
```

2. Create the config file

Create a JSON file called `config.json`. Its contents should look like:

```
  {
  	"node_id": "<NODE_ID>",
  	"access_token": "<YOUR_FACEBOOK_GRAPH_API_ACCESS_TOKEN>"
  }
```
You can quickly get a Graph API Explorer access token via the [Graph API Explorer](https://developers.facebook.com/tools/explorer?method=GET&path=me%3Ffields%3Did%2Cname&version=v2.12)

3. Run the tap
```
tap-facebook-posts -c config.json
```


4. (Optional) If you'd like to run the tap with a STATE, you should make a JSON file called `state.json`. Its contents should look like:
```
{
	"after": "<YOUR_FACEBOOK_AFTER_STATE_MARKER>"
}
```
You can then run the tap with
```
tap-facebook-posts -c config.json -s state.json
```
If you omit state settings, it will fetch all available posts for the given `node_id`.

## Development
### Running un-packaged tap
```
cd tap_facebook_posts
python . -c ../config.json
```
```
python -m tap_facebook_posts -c ../config.json
```

### Running tests
Install the requirements in `requirements-dev.txt`. Then, run the tests with `nosetests`.
You can check the tap with singer-tools using `singer-check-tap --tap tap-facebook-posts -c config.json`

### Linting
```
bash dev_tools/linter.sh
```

## Deploy
TestPyPI
```
python setup.py sdist
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
pip install --index-url https://test.pypi.org/simple/ tap-facebook-posts==0.0.2a1
```

ProdPyPI
```
twine upload dist/*
```