Skip to content

Commit a7b43c9

Browse files
clean commit
0 parents  commit a7b43c9

File tree

14 files changed

+1379
-0
lines changed

14 files changed

+1379
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Changelog
2+
3+
## 0.3.1
4+
* Fixes a memory issue when syncing contacts [#4](https://github.com/singer-io/tap-emarsys/pull/4)

LICENSE

Lines changed: 620 additions & 0 deletions
Large diffs are not rendered by default.

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
include LICENSE
2+
include tap_frontapp/schemas/*.json

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# tap-frontapp
2+
3+
This is a [Singer](https://singer.io) tap that produces JSON-formatted data following the [Singer spec](https://github.com/singer-io/getting-started/blob/master/SPEC.md).
4+
5+
This tap:
6+
7+
- Pulls raw data from FrontApp's [API](https://dev.frontapp.com/)
8+
- Extracts the following resources from FrontApp
9+
- [Analytics](https://dev.emarsys.com/v2/email-campaigns/list-email-campaigns)
10+
- Hourly/Daily analytics of metrics
11+
- team_table
12+
- Outputs the schema for each resource
13+
14+
## Setup
15+
16+
Building follows the conventional Singer setup:
17+
18+
python3 ./setup.py clean
19+
python3 ./setup.py build
20+
python3 ./setup.py install
21+
22+
## Configuration
23+
24+
This tap requires a `config.json` which specifies details regarding [API authentication](https://dev.frontapp.com/#authentication), a cutoff date for syncing historical data, and a time period range [daily,hourly] to control what incremental extract date ranges are. See [config.sample.json](config.sample.json) for an example.
25+
26+
Create the catalog:
27+
28+
```bash
29+
› tap-frontapp --config config.json --discover > catalog.json
30+
```
31+
32+
Then to run the extract:
33+
34+
```bash
35+
› tap-frontapp --config config.json --catalog catalog.json --state state.json
36+
```
37+
38+
Note that a typical state file looks like this:
39+
40+
```json
41+
{"bookmarks": {"team_table": {"date_to_resume": "2018-08-01 00:00:00"}}}
42+
```
43+
44+
---
45+
46+
Copyright © 2018 Stitch

example.config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"token": "<myapitoken>",
3+
"start_date": "2018-01-01T00:00:00Z",
4+
"metric": "team_table",
5+
"incremental_range": "daily"
6+
}

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[metadata]
2+
description-file = README.md

setup.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python
2+
3+
from setuptools import setup, find_packages
4+
5+
setup(
6+
name="tap-frontapp",
7+
version="0.3.1",
8+
description="Singer.io tap for extracting data from the FrontApp API",
9+
author="bytcode.io",
10+
url="http://singer.io",
11+
classifiers=["Programming Language :: Python :: 3 :: Only"],
12+
install_requires=[
13+
"singer-python>=5.1.1",
14+
"pendulum",
15+
"ratelimit",
16+
"backoff",
17+
"requests",
18+
],
19+
entry_points="""
20+
[console_scripts]
21+
tap-frontapp=tap_frontapp:main
22+
""",
23+
packages=find_packages(),
24+
package_data = {
25+
"schemas": ["tap_frontapp/schemas/*.json"]
26+
},
27+
include_package_data=True
28+
)

stitch_setup_documentation.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# FrontApp
2+
3+
This tap is for pulling [Analytics](https://dev.frontapp.com/#analytics) data from the FrontApp API. Its current developed scope is limited to the teams table, but it is easily expandable to the other Analytics data sets.
4+
5+
## Connecting FrontApp
6+
7+
### FrontApp Setup Requirements
8+
9+
To set up FrontApp in Stitch, you need to get your JSON web token directly from Front (go to > Plugins & API > API).
10+
11+
### Setup FrontApp as a Stitch source
12+
13+
1. [Sign into your Stitch account](https://app.stitchdata.com/)
14+
15+
2. On the Stitch Dashboard page, click the **Add Integration** button.
16+
17+
3. Click the **FrontApp** icon.
18+
19+
4. Enter a name for the integration. This is the name that will display on the Stitch Dashboard for the integration; it’ll also be used to create the schema in your destination. For example, the name "Stitch FrontApp" would create a schema called `stitch_frontapp` in the destination. **Note**: Schema names cannot be changed after you save the integration.
20+
21+
5. In the **Token** field, enter your FrontApp web token.
22+
23+
6. In the **Metric** field, enter the Analytics metric needed. The only schema supported in this tap right now is the team_table metric.
24+
25+
7. In the **Incremental Range** field, enter the desired aggregation frame (daily or hourly).
26+
27+
8. In the **Start Date** field, enter the minimum, beginning start date for FrontApp Analytics (e.g. 2017-01-1).
28+
29+
---
30+
31+
## FrontApp Replication
32+
33+
With each run of the integration, the following data set is extracted and replicated to the data warehouse:
34+
35+
- **Team Table**: Daily or hourly aggregated team member statistics since the last_update (last completed run of the integration) through the most recent day or hour respectively. On the first run, ALL increments since the **Start Date** will be replicated.
36+
37+
---
38+
39+
## FrontApp Table Schemas
40+
41+
### team_table
42+
43+
- Table name: team_table
44+
- Description: A list of team members and their event statistics during the course of the day/hour starting from the analytics_date.
45+
- Primary key: analytics_date, analytics_range, teammate_id
46+
- Replicated incrementally
47+
- Bookmark column: analytics_date (written as resume_date in the state records)
48+
- API endpoint documentation: [Analytics](https://dev.frontapp.com/#analytics)
49+
50+
---
51+
52+
## Troubleshooting / Other Important Info
53+
54+
- **Team_table Data**: The first record is for the teammate = "ALL" and so is an aggregated record across all team members. Also, the API supports pulling specific teams by using a slightly different endpoint, but we have set it up to pull members from all teams.
55+
56+
- **Timestamps**: All timestamp columns and resume_date state parameter are Unix timestamps.
57+

tap_frontapp/__init__.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
import json
6+
7+
import singer
8+
from singer import utils
9+
from singer.catalog import Catalog, CatalogEntry, Schema
10+
from . import streams
11+
from .context import Context
12+
from . import schemas
13+
14+
REQUIRED_CONFIG_KEYS = ["token", "metric"]
15+
16+
LOGGER = singer.get_logger()
17+
18+
#def check_authorization(atx):
19+
# atx.client.get('/settings')
20+
21+
22+
# with tap-emarsys, they do it this way where the catalog is read in from a call to the api
23+
# but with the odd frontapp structure, we won't do that here
24+
# we never use atx in here since the schema is from file
25+
# but we would use it if we pulled schema from the API
26+
# def discover(atx):
27+
def discover():
28+
catalog = Catalog([])
29+
for tap_stream_id in schemas.STATIC_SCHEMA_STREAM_IDS:
30+
#print("tap stream id=",tap_stream_id)
31+
schema = Schema.from_dict(schemas.load_schema(tap_stream_id))
32+
metadata = []
33+
if schema.selected is True:
34+
metadata.append({
35+
'metadata': {
36+
'selected': True
37+
},
38+
'breadcrumb': []
39+
})
40+
for field_name in schema.properties.keys():
41+
#print("field name=",field_name)
42+
if field_name in schemas.PK_FIELDS[tap_stream_id]:
43+
inclusion = 'automatic'
44+
else:
45+
inclusion = 'available'
46+
metadata.append({
47+
'metadata': {
48+
'inclusion': inclusion
49+
},
50+
'breadcrumb': ['properties', field_name]
51+
})
52+
catalog.streams.append(CatalogEntry(
53+
stream=tap_stream_id,
54+
tap_stream_id=tap_stream_id,
55+
key_properties=schemas.PK_FIELDS[tap_stream_id],
56+
schema=schema,
57+
metadata=metadata
58+
))
59+
return catalog
60+
61+
62+
# this is already defined in schemas.py though w/o dependencies. do we keep this for the sync?
63+
def load_schema(tap_stream_id):
64+
path = "schemas/{}.json".format(tap_stream_id)
65+
schema = utils.load_json(get_abs_path(path))
66+
dependencies = schema.pop("tap_schema_dependencies", [])
67+
refs = {}
68+
for sub_stream_id in dependencies:
69+
refs[sub_stream_id] = load_schema(sub_stream_id)
70+
if refs:
71+
singer.resolve_schema_references(schema, refs)
72+
return schema
73+
74+
75+
def sync(atx):
76+
for tap_stream_id in schemas.STATIC_SCHEMA_STREAM_IDS:
77+
schemas.load_and_write_schema(tap_stream_id)
78+
79+
streams.sync_selected_streams(atx)
80+
81+
82+
@utils.handle_top_exception(LOGGER)
83+
def main():
84+
args = utils.parse_args(REQUIRED_CONFIG_KEYS)
85+
atx = Context(args.config, args.state)
86+
if args.discover:
87+
# the schema is static from file so we don't need to pass in atx for connection info.
88+
catalog = discover()
89+
json.dump(catalog.to_dict(), sys.stdout)
90+
else:
91+
atx.catalog = Catalog.from_dict(args.properties) \
92+
if args.properties else discover()
93+
sync(atx)
94+
95+
if __name__ == "__main__":
96+
main()

tap_frontapp/context.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from datetime import datetime, date
2+
3+
import singer
4+
from singer import bookmarks as bks_, metadata
5+
6+
from .http import Client
7+
8+
class Context(object):
9+
"""Represents a collection of global objects necessary for performing
10+
discovery or for running syncs. Notably, it contains
11+
12+
- config - The JSON structure from the config.json argument
13+
- state - The mutable state dict that is shared among streams
14+
- client - An HTTP client object for interacting with the API
15+
- catalog - A singer.catalog.Catalog. Note this will be None during
16+
discovery.
17+
"""
18+
def __init__(self, config, state):
19+
self.config = config
20+
self.state = state
21+
self.client = Client(config)
22+
self._catalog = None
23+
self.selected_stream_ids = None
24+
self.now = datetime.utcnow()
25+
26+
@property
27+
def catalog(self):
28+
return self._catalog
29+
30+
@catalog.setter
31+
def catalog(self, catalog):
32+
self._catalog = catalog
33+
self.selected_stream_ids = set()
34+
for stream in catalog.streams:
35+
mdata = metadata.to_map(stream.metadata)
36+
root_metadata = mdata.get(())
37+
if root_metadata and root_metadata.get('selected') is True:
38+
self.selected_stream_ids.add(stream.tap_stream_id)
39+
40+
def get_bookmark(self, path):
41+
return bks_.get_bookmark(self.state, *path)
42+
43+
def set_bookmark(self, path, val):
44+
if isinstance(val, date):
45+
val = val.isoformat()
46+
bks_.write_bookmark(self.state, path[0], path[1], val)
47+
48+
def get_offset(self, path):
49+
off = bks_.get_offset(self.state, path[0])
50+
return (off or {}).get(path[1])
51+
52+
def set_offset(self, path, val):
53+
bks_.set_offset(self.state, path[0], path[1], val)
54+
55+
def clear_offsets(self, tap_stream_id):
56+
bks_.clear_offset(self.state, tap_stream_id)
57+
58+
def write_state(self):
59+
singer.write_state(self.state)

0 commit comments

Comments
 (0)