From ca87f8b462e194c38cf93e38001613dd3a63b405 Mon Sep 17 00:00:00 2001 From: Steven Hernandez Date: Tue, 14 Aug 2018 11:24:41 -0400 Subject: [PATCH] Upgrade to Api v2 (#20) * begin transition from harvest api v1 to v2 * update authentication method required for new v2 of the api * add additional required config variables for tap usage * update access_token request to comply with v2 of api * update schemas to reflect data returned from the v2 api * add additional schemas to reflect additional data points available @See https://help.getharvest.com/api-v2/ @See https://github.com/singer-io/tap-harvest/issues/9 * generify `sync_endpoint` so that it is the only method that loads data and loops through data, thus removing redundancies * allow each parent object loaded from the `sync_endpoint` to recursively sync additional endpoints (related to the parent object) through a `for_each_handler` * remove api requests from the individual `sync_$item` methods, instead prefer using the generic `sync_endpoint` method * store the id of nested objects instead of the full nested object * automatically paginate all "collection" results (all calls to the `sync_endpoint` method) * allow alternative key_properties, updated documentation, minor fixes * allow key_properties to be set to anything other than just `["id"]` for use with pivot table style data * update config.json and state.json documentation in README.md * add additional schemas and subsequent loading-code that was missed in the first pass * additional minor fixes such as * incorrect schema attribute names * missing attributes * incorrect schema attribute type/formats * minor formatting tweaks * skip syncing any endpoints that are not enabled by the given user's company, otherwise the user receives a 403 response error * load company metadata from harvest api * ensure specific features are either enabled or disabled before beginning endpoint syncs * update module to v2.0.0, minor tweaks * define default key_properties directly in `load_and_write_schema` method signature * update version * fix other minor inconsistencies * determine account_id automatically as opposed to requiring account_id from user config --- .gitignore | 1 + README.md | 28 +- setup.py | 19 +- tap_harvest/__init__.py | 514 +++++++++++------- tap_harvest/schemas/clients.json | 27 +- tap_harvest/schemas/contacts.json | 10 +- .../schemas/estimate_item_categories.json | 19 + tap_harvest/schemas/estimate_line_items.json | 32 ++ .../schemas/estimate_message_recipients.json | 20 + tap_harvest/schemas/estimate_messages.json | 43 ++ tap_harvest/schemas/estimates.json | 80 +++ tap_harvest/schemas/expense_categories.json | 6 +- tap_harvest/schemas/expenses.json | 64 ++- tap_harvest/schemas/external_reference.json | 23 + .../schemas/invoice_item_categories.json | 12 +- tap_harvest/schemas/invoice_line_items.json | 35 ++ .../schemas/invoice_message_recipients.json | 20 + tap_harvest/schemas/invoice_messages.json | 48 +- tap_harvest/schemas/invoice_payments.json | 26 +- tap_harvest/schemas/invoices.json | 106 ++-- tap_harvest/schemas/project_tasks.json | 13 +- tap_harvest/schemas/project_users.json | 15 +- tap_harvest/schemas/projects.json | 54 +- tap_harvest/schemas/roles.json | 19 + tap_harvest/schemas/tasks.json | 18 +- tap_harvest/schemas/time_entries.json | 73 ++- .../time_entry_external_reference.json | 11 + tap_harvest/schemas/user_project_tasks.json | 11 + tap_harvest/schemas/user_projects.json | 37 ++ tap_harvest/schemas/user_roles.json | 11 + .../schemas/{people.json => users.json} | 56 +- 31 files changed, 1013 insertions(+), 438 deletions(-) create mode 100644 tap_harvest/schemas/estimate_item_categories.json create mode 100644 tap_harvest/schemas/estimate_line_items.json create mode 100644 tap_harvest/schemas/estimate_message_recipients.json create mode 100644 tap_harvest/schemas/estimate_messages.json create mode 100644 tap_harvest/schemas/estimates.json create mode 100644 tap_harvest/schemas/external_reference.json create mode 100644 tap_harvest/schemas/invoice_line_items.json create mode 100644 tap_harvest/schemas/invoice_message_recipients.json create mode 100644 tap_harvest/schemas/roles.json create mode 100644 tap_harvest/schemas/time_entry_external_reference.json create mode 100644 tap_harvest/schemas/user_project_tasks.json create mode 100644 tap_harvest/schemas/user_projects.json create mode 100644 tap_harvest/schemas/user_roles.json rename tap_harvest/schemas/{people.json => users.json} (80%) diff --git a/.gitignore b/.gitignore index 8e101be..00309ac 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,5 @@ ENV/ # Custom stuff env.sh config.json +state.json .autoenv.zsh diff --git a/README.md b/README.md index 98b0580..e9f11a4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ A singer.io tap for extracting data from the Harvest REST API, written in python 3. -Author: Jordan Ryan (jordan@facetinteractive.com) +API V1 Author: Jordan Ryan (jordan@facetinteractive.com) +API V2 Author: Steven Hernandez (steven.hernandez@fostermade.co) ## Quick start @@ -11,7 +12,7 @@ Author: Jordan Ryan (jordan@facetinteractive.com) Clone this repository, and then install using setup.py. We recommend using a virtualenv: ```bash - > virtualenv -p python 3 venv + > virtualenv -p python3 venv > source venv/bin/activate > python setup.py install ``` @@ -24,7 +25,7 @@ Author: Jordan Ryan (jordan@facetinteractive.com) "client_secret": "OAUTH_CLIENT_SECRET", "refresh_token": "YOUR_OAUTH_REFRESH_TOKEN", "start_date": "2017-04-19T13:37:30Z", - "account_name": "YOUR_ACCOUNT_NAME" + "user_agent": "MyApp (your.email@example.com)" } ``` @@ -34,18 +35,23 @@ Author: Jordan Ryan (jordan@facetinteractive.com) { "clients": "2000-01-01T00:00:00Z", "contacts": "2000-01-01T00:00:00Z", - "invoices": "2000-01-01T00:00:00Z", + "estimate_item_categories": "2000-01-01T00:00:00Z", + "estimate_messages": "2000-01-01T00:00:00Z", + "estimates": "2000-01-01T00:00:00Z", + "expense_categories": "2000-01-01T00:00:00Z", + "expenses": "2000-01-01T00:00:00Z", "invoice_item_categories": "2000-01-01T00:00:00Z", - "invoice_payments": "2000-01-01T00:00:00Z", "invoice_messages": "2000-01-01T00:00:00Z", - "expenses": "2000-01-01T00:00:00Z", - "expense_categories": "2000-01-01T00:00:00Z", - "projects": "2000-01-01T00:00:00Z", + "invoice_payments": "2000-01-01T00:00:00Z", + "invoices": "2000-01-01T00:00:00Z", + "project_tasks": "2000-01-01T00:00:00Z", "project_users": "2000-01-01T00:00:00Z", + "projects": "2000-01-01T00:00:00Z", + "roles": "2000-01-01T00:00:00Z", "tasks": "2000-01-01T00:00:00Z", - "project_tasks": "2000-01-01T00:00:00Z", - "people": "2000-01-01T00:00:00Z", - "time_entries": "2000-01-01T00:00:00Z" + "time_entries": "2000-01-01T00:00:00Z", + "user_projects": "2000-01-01T00:00:00Z", + "users": "2000-01-01T00:00:00Z" } ``` diff --git a/setup.py b/setup.py index 9343e96..67f033f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup(name='tap-harvest', - version="1.1.1", + version="2.0.0", description='Singer.io tap for extracting data from the Harvest api', author='Facet Interactive', url='http://singer.io', @@ -24,17 +24,30 @@ 'tap_harvest/schemas': [ "clients.json", "contacts.json", + "estimate_item_categories.json", + "estimate_line_items.json", + "estimate_messages.json", + "estimate_message_recipients.json", + "estimates.json", "expense_categories.json", "expenses.json", + "external_reference.json", "invoice_item_categories.json", - "invoice_payments.json" + "invoice_messages.json", + "invoice_payments.json", + "invoice_message_recipients.json", "invoices.json", - "people.json", "project_tasks.json", "project_users.json", "projects.json", + "roles.json", "tasks.json", "time_entries.json", + "time_entry_external_reference.json", + "user_project_tasks.json", + "user_projects.json", + "user_roles.json", + "users.json", ], }, include_package_data=True, diff --git a/tap_harvest/__init__.py b/tap_harvest/__init__.py index 5d98ec0..4212820 100644 --- a/tap_harvest/__init__.py +++ b/tap_harvest/__init__.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import datetime import os import backoff @@ -17,34 +16,39 @@ "refresh_token", "client_id", "client_secret", - "account_name", + "user_agent", ] -BASE_URL = "https://{}.harvestapp.com/" +BASE_API_URL = "https://api.harvestapp.com/v2/" +BASE_ID_URL = "https://id.getharvest.com/api/v2/" CONFIG = {} STATE = {} AUTH = {} + class Auth: def __init__(self, client_id, client_secret, refresh_token): self._client_id = client_id self._client_secret = client_secret self._refresh_token = refresh_token + self._account_id = None self._refresh_access_token() @backoff.on_exception( backoff.expo, - (requests.exceptions.RequestException), + requests.exceptions.RequestException, max_tries=5, giveup=lambda e: e.response is not None and 400 <= e.response.status_code < 500, factor=2) def _make_refresh_token_request(self): return requests.request('POST', - url='https://api.harvestapp.com/oauth2/token', - data={'client_id': self._client_id, - 'client_secret': self._client_secret, - 'refresh_token': self._refresh_token, - 'grant_type': 'refresh_token'}, + url=BASE_ID_URL + 'oauth2/token', + data={ + 'client_id': self._client_id, + 'client_secret': self._client_secret, + 'refresh_token': self._refresh_token, + 'grant_type': 'refresh_token', + }, headers={"User-Agent": CONFIG.get("user_agent")}) def _refresh_access_token(self): @@ -65,12 +69,27 @@ def _refresh_access_token(self): LOGGER.info("Got refreshed access token") def get_access_token(self): - if (self._access_token is not None and self._expires_at > pendulum.now()): + if self._access_token is not None and self._expires_at > pendulum.now(): return self._access_token self._refresh_access_token() return self._access_token + def get_account_id(self): + if self._account_id is not None: + return self._account_id + + response = requests.request('GET', + url=BASE_ID_URL + 'accounts', + data={ + 'access_token': self._access_token, + }, + headers={"User-Agent": CONFIG.get("user_agent")}) + + self._account_id = str(response.json()['accounts'][0]['id']) + + return self._account_id + def get_abs_path(path): return os.path.join(os.path.dirname(os.path.realpath(__file__)), path) @@ -80,6 +99,12 @@ def load_schema(entity): return utils.load_json(get_abs_path("schemas/{}.json".format(entity))) +def load_and_write_schema(name, key_properties='id', bookmark_property='updated_at'): + schema = load_schema(name) + singer.write_schema(name, schema, key_properties, bookmark_properties=[bookmark_property]) + return schema + + def get_start(key): if key not in STATE: STATE[key] = CONFIG['start_date'] @@ -88,11 +113,12 @@ def get_start(key): def get_url(endpoint): - return BASE_URL.format(CONFIG['account_name']) + endpoint + return BASE_API_URL + endpoint + @backoff.on_exception( backoff.expo, - (requests.exceptions.RequestException), + requests.exceptions.RequestException, max_tries=5, giveup=lambda e: e.response is not None and 400 <= e.response.status_code < 500, factor=2) @@ -101,6 +127,7 @@ def request(url, params=None): params = params or {} access_token = AUTH.get_access_token() headers = {"Accept": "application/json", + "Harvest-Account-Id": AUTH.get_account_id(), "Authorization": "Bearer " + access_token, "User-Agent": CONFIG.get("user_agent")} req = requests.Request("GET", url=url, params=params, headers=headers).prepare() @@ -109,233 +136,343 @@ def request(url, params=None): resp.raise_for_status() return resp.json() + def append_times_to_dates(item, date_fields): if date_fields: for date_field in date_fields: if item.get(date_field): item[date_field] += "T00:00:00Z" -def sync_endpoint(endpoint, path, date_fields=None): - schema = load_schema(endpoint) + +def get_company(): + url = get_url('company') + return request(url) + + +def sync_endpoint(schema_name, endpoint=None, path=None, date_fields=None, with_updated_since=True, + for_each_handler=None, map_handler=None, object_to_id=None): + schema = load_schema(schema_name) bookmark_property = 'updated_at' - singer.write_schema(endpoint, + singer.write_schema(schema_name, schema, ["id"], bookmark_properties=[bookmark_property]) - start = get_start(endpoint) - - url = get_url(endpoint) - data = request(url) - time_extracted = utils.now() + start = get_start(schema_name) + start_dt = pendulum.parse(start) + updated_since = start_dt.strftime("%Y-%m-%dT%H:%M:%SZ") with Transformer() as transformer: - for row in data: - item = row[path] - item = transformer.transform(item, schema) + page = 1 + while page is not None: + url = get_url(endpoint or schema_name) + params = {"updated_since": updated_since} if with_updated_since else {} + params['page'] = page + response = request(url, params) + path = path or schema_name + data = response[path] + time_extracted = utils.now() - append_times_to_dates(item, date_fields) + for row in data: + if map_handler is not None: + row = map_handler(row) - if item[bookmark_property] >= start: - singer.write_record(endpoint, - item, - time_extracted=time_extracted) + if object_to_id is not None: + for key in object_to_id: + if row[key] is not None: + row[key + '_id'] = row[key]['id'] + else: + row[key + '_id'] = None - utils.update_state(STATE, endpoint, item[bookmark_property]) + item = transformer.transform(row, schema) - singer.write_state(STATE) + append_times_to_dates(item, date_fields) + if item[bookmark_property] >= start: + singer.write_record(schema_name, + item, + time_extracted=time_extracted) -def sync_projects(): - bookmark_property = 'updated_at' - tasks_schema = load_schema("project_tasks") - singer.write_schema("project_tasks", - tasks_schema, - ["id"], - bookmark_properties=[bookmark_property]) + # take any additional actions required for the currently loaded endpoint + if for_each_handler is not None: + for_each_handler(row, time_extracted=time_extracted) - users_schema = load_schema("project_users") - singer.write_schema("project_users", - users_schema, - ["id"], - bookmark_properties=[bookmark_property]) + utils.update_state(STATE, schema_name, item[bookmark_property]) + page = response['next_page'] - entries_schema = load_schema("time_entries") - singer.write_schema("time_entries", - entries_schema, - ["id"], - bookmark_properties=[bookmark_property]) + singer.write_state(STATE) - schema = load_schema("projects") - singer.write_schema("projects", - schema, - ["id"], - bookmark_properties=[bookmark_property]) - start = get_start("projects") - start_dt = pendulum.parse(start) - updated_since = start_dt.strftime("%Y-%m-%d %H:%M") +def sync_time_entries(): + def for_each_time_entry(time_entry, time_extracted): + # Extract external_reference + external_reference_schema = load_and_write_schema("external_reference") + load_and_write_schema("time_entry_external_reference", + key_properties=["time_entry_id", "external_reference_id"]) + if time_entry['external_reference'] is not None: + with Transformer() as transformer: + external_reference = time_entry['external_reference'] + external_reference = transformer.transform(external_reference, + external_reference_schema) + + singer.write_record("external_reference", + external_reference, + time_extracted=time_extracted) - url = get_url("projects") - projects_data = request(url) - projects_time_extracted = utils.now() + # Create pivot row for time_entry and external_reference + pivot_row = { + 'time_entry_id': time_entry['id'], + 'external_reference_id': external_reference['id'] + } - with Transformer() as transformer: - for row in projects_data: - item = row["project"] - item = transformer.transform(item, schema) - date_fields = ["starts_on", - "ends_on", - "hint_earliest_record_at", - "hint_latest_record_at"] - - append_times_to_dates(item, date_fields) - - if item[bookmark_property] >= start: - singer.write_record("projects", - item, - time_extracted=projects_time_extracted) - - utils.update_state(STATE, "projects", item[bookmark_property]) - - suburl = url + "/{}/user_assignments".format(item["id"]) - project_users_data = request(suburl, params={"updated_since": updated_since}) - project_users_time_extracted = utils.now() - - for subrow in project_users_data: - subitem = subrow["user_assignment"] - subitem = transformer.transform(subitem, users_schema) - singer.write_record("project_users", - subitem, - time_extracted=project_users_time_extracted) - - suburl = url + "/{}/task_assignments".format(item["id"]) - task_assignments_data = request(suburl, params={"updated_since": updated_since}) - task_assignments_time_extracted = utils.now() - - for subrow in task_assignments_data: - subitem = subrow["task_assignment"] - subitem = transformer.transform(subitem, tasks_schema) - singer.write_record("project_tasks", - subitem, - time_extracted=task_assignments_time_extracted) - - suburl = url + "/{}/entries".format(item["id"]) - subparams = { - "from": start_dt.strftime("%Y%m%d"), - "to": datetime.datetime.utcnow().strftime("%Y%m%d"), - "updated_since": updated_since, - } + singer.write_record("time_entry_external_reference", + pivot_row, + time_extracted=time_extracted) - time_entries_data = request(suburl, params=subparams) - time_entries_time_extracted = utils.now() + sync_endpoint("time_entries", for_each_handler=for_each_time_entry, + object_to_id=[ + 'user', + 'user_assignment', + 'client', + 'project', + 'task', + 'task_assignment', + 'external_reference', + 'invoice' + ]) - for subrow in time_entries_data: - subitem = subrow["day_entry"] - subitem = transformer.transform(subitem, entries_schema) - singer.write_record("time_entries", - subitem, - time_extracted=time_entries_time_extracted) - singer.write_state(STATE) +def sync_invoices(): + def for_each_invoice_message(message, time_extracted): + # Extract all invoice_message_recipients + recipients_schema = load_and_write_schema("invoice_message_recipients") + with Transformer() as transformer: + for recipient in message['recipients']: + recipient['invoice_message_id'] = message['id'] + recipient = transformer.transform(recipient, recipients_schema) + + singer.write_record("invoice_message_recipients", + recipient, + time_extracted=time_extracted) + def for_each_invoice(invoice, time_extracted): + def map_invoice_message(message): + message['invoice_id'] = invoice['id'] + return message + + def map_invoice_payment(payment): + payment['invoice_id'] = invoice['id'] + payment['payment_gateway_id'] = payment['payment_gateway']['id'] + payment['payment_gateway_name'] = payment['payment_gateway']['name'] + return payment + + # Sync invoice messages + sync_endpoint("invoice_messages", + endpoint=("invoices/{}/messages".format(invoice['id'])), + path="invoice_messages", + with_updated_since=False, + map_handler=map_invoice_message, + for_each_handler=for_each_invoice_message, + date_fields=["send_reminder_on"]) + + # Sync invoice payments + sync_endpoint("invoice_payments", + endpoint=("invoices/{}/payments".format(invoice['id'])), + path="invoice_payments", + with_updated_since=False, + map_handler=map_invoice_payment, + date_fields=["send_reminder_on"]) + + # Extract all invoice_line_items + line_items_schema = load_and_write_schema("invoice_line_items") + with Transformer() as transformer: + for line_item in invoice['line_items']: + line_item['invoice_id'] = invoice['id'] + if line_item['project'] is not None: + line_item['project_id'] = line_item['project']['id'] + else: + line_item['project_id'] = None + line_item = transformer.transform(line_item, line_items_schema) + + singer.write_record("invoice_line_items", + line_item, + time_extracted=time_extracted) -def sync_invoices(): - messages_schema = load_schema("invoice_messages") - bookmark_property = 'updated_at' - singer.write_schema("invoice_messages", - messages_schema, - ["id"], - bookmark_properties=[bookmark_property]) + sync_endpoint("invoices", for_each_handler=for_each_invoice, date_fields=[ + "period_start", + "period_end", + "issue_date", + "due_date", + "paid_date", + ], object_to_id=['client', 'estimate', 'retainer', 'creator']) - payments_schema = load_schema("invoice_payments") - singer.write_schema("invoice_payments", - payments_schema, - ["id"], - bookmark_properties=[bookmark_property]) - schema = load_schema("invoices") - singer.write_schema("invoices", - schema, - ["id"], - bookmark_properties=[bookmark_property]) +def sync_estimates(): + def for_each_estimate_message(message, time_extracted): + # Extract all estimate_message_recipients + recipients_schema = load_and_write_schema("estimate_message_recipients") - start = get_start("invoices") + with Transformer() as transformer: + for recipient in message['recipients']: + recipient['estimate_message_id'] = message['id'] + recipient = transformer.transform(recipient, recipients_schema) - start_dt = pendulum.parse(start) - updated_since = start_dt.strftime("%Y-%m-%d %H:%M") + singer.write_record("estimate_message_recipients", + recipient, + time_extracted=time_extracted) - url = get_url("invoices") - with Transformer() as transformer: - while True: - data = request(url, {"updated_since": updated_since}) - invoices_time_extracted = utils.now() + def map_estimate_message(message): + message['estimate_id'] = message['id'] + return message + + def for_each_estimate(estimate, time_extracted): + # Sync estimate messages + sync_endpoint("estimate_messages", + endpoint=("estimates/{}/messages".format(estimate['id'])), + path="estimate_messages", + with_updated_since=False, + for_each_handler=for_each_estimate_message, + date_fields=["send_reminder_on"], + map_handler=map_estimate_message) + + # Extract all estimate_line_items + line_items_schema = load_and_write_schema("estimate_line_items") + with Transformer() as transformer: + for line_item in estimate['line_items']: + line_item['estimate_id'] = estimate['id'] + line_item = transformer.transform(line_item, line_items_schema) + + singer.write_record("estimate_line_items", + line_item, + time_extracted=time_extracted) + + sync_endpoint("estimates", + for_each_handler=for_each_estimate, + date_fields=["issue_date"], + object_to_id=['client', 'user']) - for row in data: - item = row["invoices"] - item = transformer.transform(item, schema) - append_times_to_dates(item, ["issued_at", "due_at"]) - singer.write_record("invoices", - item, - time_extracted=invoices_time_extracted) +def sync_roles(): + def for_each_role(role, time_extracted): + # Extract user_roles + load_and_write_schema("user_roles", key_properties=["user_id", "role_id"]) + for user_id in role['user_ids']: + pivot_row = { + 'role_id': role['id'], + 'user_id': user_id + } - utils.update_state(STATE, "invoices", item['updated_at']) + singer.write_record("user_roles", + pivot_row, + time_extracted=time_extracted) - suburl = url + "/{}/messages".format(item['id']) - messages_data = request(suburl) - messages_time_extracted = utils.now() - for subrow in messages_data: - subitem = subrow["message"] - if subitem['updated_at'] >= start: - append_times_to_dates(subitem, ["send_reminder_on"]) - singer.write_record("invoice_messages", - subitem, - time_extracted=messages_time_extracted) + sync_endpoint("roles", for_each_handler=for_each_role) - suburl = url + "/{}/payments".format(item['id']) - payments_data = request(suburl) - payments_time_extracted = utils.now() - for subrow in payments_data: - subitem = subrow["payment"] - subitem = transformer.transform(subitem, payments_schema) - if subitem['updated_at'] >= start: - singer.write_record("invoice_payments", - subitem, - time_extracted=payments_time_extracted) +def sync_users(): + def for_each_user(user, time_extracted): + def map_user_projects(project_assignment): + project_assignment['user'] = user + return project_assignment - singer.write_state(STATE) + def for_each_user_project(user_project_assignment, time_extracted): + # Extract user_project_tasks + load_and_write_schema("user_project_tasks", + key_properties=["user_id", "project_task_id"]) + for project_task in user_project_assignment['task_assignments']: + pivot_row = { + 'user_id': user['id'], + 'project_task_id': project_task['id'] + } - if len(data) < 50: - break + singer.write_record("user_project_tasks", + pivot_row, + time_extracted=time_extracted) - singer.write_state(STATE) + sync_endpoint("user_projects", + endpoint=("users/{}/project_assignments".format(user['id'])), + path="project_assignments", + with_updated_since=False, + object_to_id=['project', 'client', 'user'], + map_handler=map_user_projects, + for_each_handler=for_each_user_project) + + sync_endpoint("users", for_each_handler=for_each_user) + + +def sync_expenses(): + def map_expense(expense): + if expense['receipt'] is None: + expense['receipt_url'] = None + expense['receipt_file_name'] = None + expense['receipt_file_size'] = None + expense['receipt_content_type'] = None + else: + expense['receipt_url'] = expense['receipt']['url'] + expense['receipt_file_name'] = expense['receipt']['file_name'] + expense['receipt_file_size'] = expense['receipt']['file_size'] + expense['receipt_content_type'] = expense['receipt']['content_type'] + return expense + + sync_endpoint("expenses", + date_fields=["spent_date"], + map_handler=map_expense, + object_to_id=[ + 'client', + 'project', + 'expense_category', + 'user', + 'user_assignment', + 'invoice' + ]) def do_sync(): LOGGER.info("Starting sync") + company = get_company() + # Grab all clients and client contacts. Contacts have client FKs so grab # them last. - sync_endpoint("clients", "client") - sync_endpoint("contacts", "contact") - - # Get all people and tasks before grabbing the projects. When we grab the - # projects we will grab the project_users, project_tasks, and time_entries - # for each. - sync_endpoint("people", "user") - sync_endpoint("tasks", "task") - sync_projects() - - # Sync expenses and their categories - sync_endpoint("expense_categories", "expense_category") - sync_endpoint("expenses", "expense", date_fields=["spent_at"]) - - # Sync invoices and all related records - sync_endpoint("invoice_item_categories", "invoice_category") - sync_invoices() + sync_endpoint("clients") + sync_endpoint("contacts", object_to_id=['client']) + sync_roles() + + # Sync related project objects + sync_endpoint("projects", object_to_id=['client']) + sync_endpoint("tasks") + sync_endpoint("project_tasks", endpoint='task_assignments', path='task_assignments', + object_to_id=['project', 'task']) + sync_endpoint("project_users", endpoint='user_assignments', path='user_assignments', + object_to_id=['project', 'user']) + + # Sync users + sync_users() + + if company['expense_feature']: + # Sync expenses and their categories + sync_endpoint("expense_categories") + sync_expenses() + else: + LOGGER.info("Expense Feature not enabled, skipping.") + + if company['invoice_feature']: + # Sync invoices and all related records + sync_endpoint("invoice_item_categories") + sync_invoices() + else: + LOGGER.info("Invoice Feature not enabled, skipping.") + + if company['estimate_feature']: + # Sync estimates and all related records + sync_endpoint("estimate_item_categories") + sync_estimates() + else: + LOGGER.info("Estimate Feature not enabled, skipping.") + + # Sync Time Entries along with their external reference objects + sync_time_entries() LOGGER.info("Sync complete") @@ -343,11 +480,12 @@ def do_sync(): def main_impl(): args = utils.parse_args(REQUIRED_CONFIG_KEYS) CONFIG.update(args.config) - global AUTH # pylint: disable=global-statement + global AUTH # pylint: disable=global-statement AUTH = Auth(CONFIG['client_id'], CONFIG['client_secret'], CONFIG['refresh_token']) STATE.update(args.state) do_sync() + def main(): try: main_impl() diff --git a/tap_harvest/schemas/clients.json b/tap_harvest/schemas/clients.json index eedeb42..e9f94b6 100644 --- a/tap_harvest/schemas/clients.json +++ b/tap_harvest/schemas/clients.json @@ -7,37 +7,22 @@ "name": { "type": ["null", "string"] }, - "active": { + "is_active": { "type": ["null", "boolean"] }, - "currency": { + "address": { "type": ["null", "string"] }, - "highrise_id": { - "type": ["null", "boolean"] - }, - "cache_version": { - "type": ["null", "integer"] + "currency": { + "type": ["null", "string"] }, - "updated_at": { + "created_at": { "type": ["null", "string"], "format": "date-time" }, - "created_at": { + "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "currency_symbol": { - "type": ["null", "string"] - }, - "details": { - "type": ["null", "string"] - }, - "default_invoice_timeframe": { - "type": ["null", "string"] - }, - "last_invoice_kind": { - "type": ["null", "string"] } } } diff --git a/tap_harvest/schemas/contacts.json b/tap_harvest/schemas/contacts.json index 2267333..dc5c32c 100644 --- a/tap_harvest/schemas/contacts.json +++ b/tap_harvest/schemas/contacts.json @@ -7,6 +7,9 @@ "client_id": { "type": ["null", "integer"] }, + "title": { + "type": ["null", "string"] + }, "first_name": { "type": ["null", "string"] }, @@ -25,14 +28,11 @@ "fax": { "type": ["null", "string"] }, - "title": { - "type": ["null", "string"] - }, - "updated_at": { + "created_at": { "type": ["null", "string"], "format": "date-time" }, - "created_at": { + "updated_at": { "type": ["null", "string"], "format": "date-time" } diff --git a/tap_harvest/schemas/estimate_item_categories.json b/tap_harvest/schemas/estimate_item_categories.json new file mode 100644 index 0000000..dcfa82a --- /dev/null +++ b/tap_harvest/schemas/estimate_item_categories.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/tap_harvest/schemas/estimate_line_items.json b/tap_harvest/schemas/estimate_line_items.json new file mode 100644 index 0000000..4d86539 --- /dev/null +++ b/tap_harvest/schemas/estimate_line_items.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "estimate_id": { + "type": ["null", "integer"] + }, + "kind": { + "type": ["null", "string"] + }, + "description": { + "type": ["null", "string"] + }, + "quantity": { + "type": ["null", "integer"] + }, + "unit_price": { + "type": ["null", "number"] + }, + "amount": { + "type": ["null", "number"] + }, + "taxed": { + "type": ["null", "boolean"] + }, + "taxed2": { + "type": ["null", "boolean"] + } + } +} diff --git a/tap_harvest/schemas/estimate_message_recipients.json b/tap_harvest/schemas/estimate_message_recipients.json new file mode 100644 index 0000000..b84475a --- /dev/null +++ b/tap_harvest/schemas/estimate_message_recipients.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "email": { + "type": ["null", "string"] + }, + "invoice_id": { + "type": ["null", "integer"] + }, + "invoice_message_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/estimate_messages.json b/tap_harvest/schemas/estimate_messages.json new file mode 100644 index 0000000..9e3239b --- /dev/null +++ b/tap_harvest/schemas/estimate_messages.json @@ -0,0 +1,43 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "sent_by": { + "type": ["null", "string"] + }, + "sent_by_email": { + "type": ["null", "string"] + }, + "sent_from": { + "type": ["null", "string"] + }, + "sent_from_email": { + "type": ["null", "string"] + }, + "subject": { + "type": ["null", "string"] + }, + "body": { + "type": ["null", "string"] + }, + "send_me_a_copy": { + "type": ["null", "boolean"] + }, + "event_type": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "estimate_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/estimates.json b/tap_harvest/schemas/estimates.json new file mode 100644 index 0000000..9f5cd80 --- /dev/null +++ b/tap_harvest/schemas/estimates.json @@ -0,0 +1,80 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "client_id": { + "type": ["null", "integer"] + }, + "creator_id": { + "type": ["null", "integer"] + }, + "client_key": { + "type": ["null", "string"] + }, + "number": { + "type": ["null", "string"] + }, + "purchase_order": { + "type": ["null", "string"] + }, + "amount": { + "type": ["null", "number"] + }, + "tax": { + "type": ["null", "string", "number"] + }, + "tax_amount": { + "type": ["null", "number"] + }, + "tax2": { + "type": ["null", "string", "number"] + }, + "tax2_amount": { + "type": ["null", "number"] + }, + "discount": { + "type": ["null", "string", "number"] + }, + "discount_amount": { + "type": ["null", "number"] + }, + "subject": { + "type": ["null", "string"] + }, + "notes": { + "type": ["null", "string"] + }, + "currency": { + "type": ["null", "string"] + }, + "state": { + "type": ["null", "string"] + }, + "issue_date": { + "type": ["null", "string"], + "format": "date-time" + }, + "sent_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "accepted_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "declined_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/tap_harvest/schemas/expense_categories.json b/tap_harvest/schemas/expense_categories.json index 4d6fd5f..1ecaeac 100644 --- a/tap_harvest/schemas/expense_categories.json +++ b/tap_harvest/schemas/expense_categories.json @@ -13,6 +13,9 @@ "unit_price": { "type": ["null", "number"] }, + "is_active": { + "type": ["null", "boolean"] + }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -20,9 +23,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "deactivated": { - "type": ["null","boolean"] } } } diff --git a/tap_harvest/schemas/expenses.json b/tap_harvest/schemas/expenses.json index f712672..08c8722 100644 --- a/tap_harvest/schemas/expenses.json +++ b/tap_harvest/schemas/expenses.json @@ -4,22 +4,8 @@ "id": { "type": ["null", "integer"] }, - "total_cost": { - "type": ["null", "number"] - }, - "units": { - "type": ["null", "number"] - }, - "notes": { - "type": ["null", "string"] - }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" - }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "client_id": { + "type": ["null", "integer"] }, "project_id": { "type": ["null", "integer"] @@ -30,35 +16,59 @@ "user_id": { "type": ["null", "integer"] }, - "spent_at": { + "user_assignment_id": { + "type": ["null", "integer"] + }, + "receipt_url": { "type": ["null", "string"] }, - "is_closed": { - "type": ["null", "boolean"] + "receipt_file_name": { + "type": ["null", "string"] }, - "notes": { + "receipt_file_size": { + "type": ["null", "integer"] + }, + "receipt_content_type": { "type": ["null", "string"] }, "invoice_id": { "type": ["null", "integer"] }, + "notes": { + "type": ["null", "string"] + }, "billable": { "type": ["null", "boolean"] }, - "company_id": { - "type": ["null", "integer"] - }, - "has_receipt": { + "is_closed": { "type": ["null", "boolean"] }, - "receipt_url": { - "type": ["null", "string"] - }, "is_locked": { "type": ["null", "boolean"] }, + "is_billed": { + "type": ["null", "boolean"] + }, "locked_reason": { "type": ["null", "string"] + }, + "spent_date": { + "type": ["null", "string"], + "format": "date-time" + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "total_cost": { + "type": ["null", "number"] + }, + "units": { + "type": ["null", "number"] } } } diff --git a/tap_harvest/schemas/external_reference.json b/tap_harvest/schemas/external_reference.json new file mode 100644 index 0000000..776e2d5 --- /dev/null +++ b/tap_harvest/schemas/external_reference.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "task_id": { + "type": ["null", "integer"] + }, + "group_id": { + "type": ["null", "integer"] + }, + "permalink": { + "type": ["null", "string"] + }, + "service": { + "type": ["null", "string"] + }, + "service_icon_url": { + "type": ["null", "string"] + } + } +} diff --git a/tap_harvest/schemas/invoice_item_categories.json b/tap_harvest/schemas/invoice_item_categories.json index 1025754..65410e8 100644 --- a/tap_harvest/schemas/invoice_item_categories.json +++ b/tap_harvest/schemas/invoice_item_categories.json @@ -7,6 +7,12 @@ "name": { "type": ["null", "string"] }, + "use_as_service": { + "type": ["null", "boolean"] + }, + "use_as_expense": { + "type": ["null", "boolean"] + }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -14,12 +20,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "use_as_service": { - "type": ["null","boolean"] - }, - "use_as_expense": { - "type": ["null","boolean"] } } } diff --git a/tap_harvest/schemas/invoice_line_items.json b/tap_harvest/schemas/invoice_line_items.json new file mode 100644 index 0000000..7afe9bc --- /dev/null +++ b/tap_harvest/schemas/invoice_line_items.json @@ -0,0 +1,35 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "project_id": { + "type": ["null", "integer"] + }, + "kind": { + "type": ["null", "string"] + }, + "description": { + "type": ["null", "string"] + }, + "quantity": { + "type": ["null", "integer"] + }, + "unit_price": { + "type": ["null", "number"] + }, + "amount": { + "type": ["null", "number"] + }, + "taxed": { + "type": ["null", "boolean"] + }, + "taxed2": { + "type": ["null", "boolean"] + }, + "invoice_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/invoice_message_recipients.json b/tap_harvest/schemas/invoice_message_recipients.json new file mode 100644 index 0000000..b84475a --- /dev/null +++ b/tap_harvest/schemas/invoice_message_recipients.json @@ -0,0 +1,20 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "email": { + "type": ["null", "string"] + }, + "invoice_id": { + "type": ["null", "integer"] + }, + "invoice_message_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/invoice_messages.json b/tap_harvest/schemas/invoice_messages.json index 6ea537b..e6e30ae 100644 --- a/tap_harvest/schemas/invoice_messages.json +++ b/tap_harvest/schemas/invoice_messages.json @@ -4,50 +4,56 @@ "id": { "type": ["null", "integer"] }, - "invoice_id": { - "type": ["null", "integer"] + "sent_by": { + "type": ["null", "string"] }, - "send_me_a_copy": { - "type": ["null", "boolean"] + "sent_by_email": { + "type": ["null", "string"] }, - "body": { + "sent_from": { "type": ["null", "string"] }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" + "sent_from_email": { + "type": ["null", "string"] }, - "sent_by": { + "subject": { "type": ["null", "string"] }, - "sent_by_email": { + "body": { "type": ["null", "string"] }, + "include_link_to_client_invoice": { + "type": ["null", "boolean"] + }, + "attach_pdf": { + "type": ["null", "boolean"] + }, + "send_me_a_copy": { + "type": ["null", "boolean"] + }, "thank_you": { "type": ["null", "boolean"] }, - "subject": { + "event_type": { "type": ["null", "string"] }, - "include_pay_pal_link": { + "reminder": { "type": ["null", "boolean"] }, - "updated_at": { + "send_reminder_on": { "type": ["null", "string"], "format": "date-time" }, - "sent_from_email": { - "type": ["null", "string"] - }, - "sent_from": { - "type": ["null", "string"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" }, - "send_reminder_on": { + "updated_at": { "type": ["null", "string"], "format": "date-time" }, - "full_recipient_list": { - "type": ["null", "string"] + "invoice_id": { + "type": ["null", "integer"] } } } diff --git a/tap_harvest/schemas/invoice_payments.json b/tap_harvest/schemas/invoice_payments.json index 9cf5f0c..52fb7d6 100644 --- a/tap_harvest/schemas/invoice_payments.json +++ b/tap_harvest/schemas/invoice_payments.json @@ -4,41 +4,45 @@ "id": { "type": ["null", "integer"] }, - "invoice_id": { - "type": ["null", "integer"] - }, "amount": { - "type": ["null","number"] + "type": ["null", "number"] }, "paid_at": { "type": ["null", "string"], "format": "date-time" }, - "created_at": { + "paid_date": { "type": ["null", "string"], "format": "date-time" }, - "notes": { - "type": ["null", "string"] - }, "recorded_by": { "type": ["null", "string"] }, "recorded_by_email": { "type": ["null", "string"] }, - "paypal_transaction_id": { - "type": ["null", "integer"] + "notes": { + "type": ["null", "string"] }, - "authorization": { + "transaction_id": { "type": ["null", "string"] }, "payment_gateway_id": { "type": ["null", "integer"] }, + "payment_gateway_name": { + "type": ["null", "integer"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, "updated_at": { "type": ["null", "string"], "format": "date-time" + }, + "invoice_id": { + "type": ["null", "integer"] } } } diff --git a/tap_harvest/schemas/invoices.json b/tap_harvest/schemas/invoices.json index e127417..23bd368 100644 --- a/tap_harvest/schemas/invoices.json +++ b/tap_harvest/schemas/invoices.json @@ -7,88 +7,102 @@ "client_id": { "type": ["null", "integer"] }, - "period_start": { - "type": ["null", "string"] + "estimate_id": { + "type": ["null", "integer"] }, - "period_end": { - "type": ["null", "string"] + "retainer_id": { + "type": ["null", "integer"] }, - "number": { + "creator_id": { + "type": ["null", "integer"] + }, + "client_key": { "type": ["null", "string"] }, - "issued_at": { + "number": { "type": ["null", "string"] }, - "due_at": { + "purchase_order": { "type": ["null", "string"] }, "amount": { "type": ["null", "number"] }, - "currency": { - "type": ["null", "string"] + "due_amount": { + "type": ["null", "number"] }, - "state": { + "tax": { + "type": ["null", "string", "number"] + }, + "tax_amount": { + "type": ["null", "number"] + }, + "tax2": { + "type": ["null", "string", "number"] + }, + "tax2_amount": { + "type": ["null", "number"] + }, + "discount": { + "type": ["null", "string", "number"] + }, + "discount_amount": { + "type": ["null", "number"] + }, + "subject": { "type": ["null", "string"] }, "notes": { "type": ["null", "string"] }, - "purchase_order": { + "currency": { "type": ["null", "string"] }, - "due_amount": { - "type": ["null", "number"] - }, - "due_at_human_format": { + "state": { "type": ["null", "string"] }, - "created_at": { + "period_start": { "type": ["null", "string"], "format": "date-time" }, - "updated_at": { + "period_end": { "type": ["null", "string"], "format": "date-time" }, - "tax": { - "type": ["null", "string", "number"] - }, - "tax_amount": { - "type": ["null", "number"] - }, - "subject": { - "type": ["null", "string"] - }, - "recurring_invoice_id": { - "type": ["null", "integer"] - }, - "tax2": { - "type": ["null", "string", "number"] + "issue_date": { + "type": ["null", "string"], + "format": "date-time" }, - "tax2_amount": { - "type": ["null", "number"] + "due_date": { + "type": ["null", "string"], + "format": "date-time" }, - "client_key": { + "payment_term": { "type": ["null", "string"] }, - "estimate_id": { - "type": ["null", "integer"] + "sent_at": { + "type": ["null", "string"], + "format": "date-time" }, - "discount": { - "type": ["null", "string", "number"] + "paid_at": { + "type": ["null", "string"], + "format": "date-time" }, - "discount_amount": { - "type": ["null", "number"] + "paid_date": { + "type": ["null", "string"], + "format": "date-time" }, - "retainer_id": { - "type": ["null", "integer"] + "closed_at": { + "type": ["null", "string"], + "format": "date-time" }, - "created_by_id": { - "type": ["null", "integer"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" }, - "client_name": { - "type": ["null", "string"] + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } } diff --git a/tap_harvest/schemas/project_tasks.json b/tap_harvest/schemas/project_tasks.json index 4bd9eec..de0c235 100644 --- a/tap_harvest/schemas/project_tasks.json +++ b/tap_harvest/schemas/project_tasks.json @@ -1,16 +1,19 @@ { "type": "object", "properties": { + "id": { + "type": ["null", "integer"] + }, "project_id": { "type": ["null", "integer"] }, "task_id": { "type": ["null", "integer"] }, - "billable": { + "is_active": { "type": ["null", "boolean"] }, - "deactivated": { + "billable": { "type": ["null", "boolean"] }, "hourly_rate": { @@ -19,9 +22,6 @@ "budget": { "type": ["null", "number"] }, - "id": { - "type": ["null", "integer"] - }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -29,9 +29,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "estimate": { - "type": ["null", "number"] } } } diff --git a/tap_harvest/schemas/project_users.json b/tap_harvest/schemas/project_users.json index 721e629..2977468 100644 --- a/tap_harvest/schemas/project_users.json +++ b/tap_harvest/schemas/project_users.json @@ -1,24 +1,24 @@ { "type": "object", "properties": { - "user_id": { + "id": { "type": ["null", "integer"] }, "project_id": { "type": ["null", "integer"] }, - "is_project_manager": { + "user_id": { + "type": ["null", "integer"] + }, + "is_active": { "type": ["null", "boolean"] }, - "deactivated": { + "is_project_manager": { "type": ["null", "boolean"] }, "hourly_rate": { "type": ["null", "number"] }, - "id": { - "type": ["null", "integer"] - }, "budget": { "type": ["null", "number"] }, @@ -29,9 +29,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "estimate": { - "type": ["null", "number"] } } } diff --git a/tap_harvest/schemas/projects.json b/tap_harvest/schemas/projects.json index d226df3..acc4ae9 100644 --- a/tap_harvest/schemas/projects.json +++ b/tap_harvest/schemas/projects.json @@ -13,10 +13,13 @@ "code": { "type": ["null", "string"] }, - "active": { + "is_active": { "type": ["null", "boolean"] }, - "billable": { + "is_billable": { + "type": ["null", "boolean"] + }, + "is_fixed_fee": { "type": ["null", "boolean"] }, "bill_by": { @@ -31,52 +34,47 @@ "budget_by": { "type": ["null", "string"] }, + "budget_is_monthly": { + "type": ["null", "boolean"] + }, "notify_when_over_budget": { "type": ["null", "boolean"] }, - "over_budget_notification_percetange": { + "over_budget_notification_percentage": { "type": ["null", "integer"] }, - "over_budget_notified_at": { - "type": ["null", "string"] - }, - "show_budget_to_all": { - "type": ["null", "boolean"] - }, - "created_at": { + "over_budget_notification_date": { "type": ["null", "string"], "format": "date-time" }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "show_budget_to_all": { + "type": ["null", "boolean"] }, - "starts_on": { - "type": ["null", "string"] + "cost_budget": { + "type": ["null", "number"] }, - "ends_on": { - "type": ["null", "string"] + "cost_budget_include_expenses": { + "type": ["null", "boolean"] }, - "estimate": { + "fee": { "type": ["null", "number"] }, - "estimate_by": { - "type": ["null", "string"] - }, - "hint_earliest_record_at": { + "notes": { "type": ["null", "string"] }, - "hint_latest_record_at": { + "starts_on": { "type": ["null", "string"] }, - "notes": { + "ends_on": { "type": ["null", "string"] }, - "cost_budget": { - "type": ["null", "number"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" }, - "cost_budget_include_expenses": { - "type": ["null", "boolean"] + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } } diff --git a/tap_harvest/schemas/roles.json b/tap_harvest/schemas/roles.json new file mode 100644 index 0000000..dcfa82a --- /dev/null +++ b/tap_harvest/schemas/roles.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "name": { + "type": ["null", "string"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + } + } +} diff --git a/tap_harvest/schemas/tasks.json b/tap_harvest/schemas/tasks.json index 349ca98..2484b0b 100644 --- a/tap_harvest/schemas/tasks.json +++ b/tap_harvest/schemas/tasks.json @@ -10,6 +10,15 @@ "billable_by_default": { "type": ["null", "boolean"] }, + "default_hourly_rate": { + "type": ["null", "number"] + }, + "is_default": { + "type": ["null", "boolean"] + }, + "is_active": { + "type": ["null", "boolean"] + }, "created_at": { "type": ["null", "string"], "format": "date-time" @@ -17,15 +26,6 @@ "updated_at": { "type": ["null", "string"], "format": "date-time" - }, - "is_default": { - "type": ["null", "boolean"] - }, - "default_hourly_rate": { - "type": ["null", "number"] - }, - "deactivated": { - "type": ["null", "boolean"] } } } diff --git a/tap_harvest/schemas/time_entries.json b/tap_harvest/schemas/time_entries.json index c54ad8a..cee9a1b 100644 --- a/tap_harvest/schemas/time_entries.json +++ b/tap_harvest/schemas/time_entries.json @@ -4,16 +4,17 @@ "id": { "type": ["null", "integer"] }, - "notes": { - "type": ["null", "string"] + "spent_date": { + "type": ["null", "string"], + "format": "date-time" }, - "spent_at": { - "type": ["null", "string"] + "user_id": { + "type": ["null", "integer"] }, - "hours": { - "type": ["null", "number"] + "user_assignment_id": { + "type": ["null", "integer"] }, - "user_id": { + "client_id": { "type": ["null", "integer"] }, "project_id": { @@ -22,29 +23,67 @@ "task_id": { "type": ["null", "integer"] }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" + "task_assignment_id": { + "type": ["null", "integer"] }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "external_reference_id": { + "type": ["null", "integer"] + }, + "invoice_id": { + "type": ["null", "integer"] + }, + "hours": { + "type": ["null", "number"] + }, + "notes": { + "type": ["null", "string"] }, - "adjustment_record": { + "is_locked": { + "type": ["null", "boolean"] + }, + "locked_reason": { + "type": ["null", "string"] + }, + "is_closed": { + "type": ["null", "boolean"] + }, + "is_billed": { "type": ["null", "boolean"] }, "timer_started_at": { "type": ["null", "string"], "format": "date-time" }, - "is_closed": { + "started_time": { + "type": ["null", "string"], + "format": "time" + }, + "ended_time": { + "type": ["null", "string"], + "format": "time" + }, + "is_running": { "type": ["null", "boolean"] }, - "is_billed": { + "billable": { "type": ["null", "boolean"] }, - "hours_with_timer": { + "budgeted": { + "type": ["null", "number"] + }, + "billable_rate": { "type": ["null", "number"] + }, + "cost_rate": { + "type": ["null", "number"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } } diff --git a/tap_harvest/schemas/time_entry_external_reference.json b/tap_harvest/schemas/time_entry_external_reference.json new file mode 100644 index 0000000..f8b4030 --- /dev/null +++ b/tap_harvest/schemas/time_entry_external_reference.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "time_entry_id": { + "type": ["null", "integer"] + }, + "external_reference_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/user_project_tasks.json b/tap_harvest/schemas/user_project_tasks.json new file mode 100644 index 0000000..eeb1db0 --- /dev/null +++ b/tap_harvest/schemas/user_project_tasks.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "user_id": { + "type": ["null", "integer"] + }, + "project_task_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/user_projects.json b/tap_harvest/schemas/user_projects.json new file mode 100644 index 0000000..5fa1969 --- /dev/null +++ b/tap_harvest/schemas/user_projects.json @@ -0,0 +1,37 @@ +{ + "type": "object", + "properties": { + "id": { + "type": ["null", "integer"] + }, + "is_active": { + "type": ["null", "boolean"] + }, + "is_project_manager": { + "type": ["null", "boolean"] + }, + "hourly_rate": { + "type": ["null", "number"] + }, + "budget": { + "type": ["null", "number"] + }, + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "project_id": { + "type": ["null", "integer"] + }, + "client_id": { + "type": ["null", "integer"] + }, + "user_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/user_roles.json b/tap_harvest/schemas/user_roles.json new file mode 100644 index 0000000..a37137c --- /dev/null +++ b/tap_harvest/schemas/user_roles.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "user_id": { + "type": ["null", "integer"] + }, + "role_id": { + "type": ["null", "integer"] + } + } +} diff --git a/tap_harvest/schemas/people.json b/tap_harvest/schemas/users.json similarity index 80% rename from tap_harvest/schemas/people.json rename to tap_harvest/schemas/users.json index 7108349..faca4f5 100644 --- a/tap_harvest/schemas/people.json +++ b/tap_harvest/schemas/users.json @@ -4,58 +4,64 @@ "id": { "type": ["null", "integer"] }, - "email": { + "first_name": { "type": ["null", "string"] }, - "created_at": { - "type": ["null", "string"], - "format": "date-time" - }, - "is_admin": { - "type": ["null", "boolean"] + "last_name": { + "type": ["null", "string"] }, - "first_name": { + "email": { "type": ["null", "string"] }, - "last_name": { + "telephone": { "type": ["null", "string"] }, "timezone": { "type": ["null", "string"] }, + "has_access_to_all_future_projects": { + "type": ["null", "boolean"] + }, "is_contractor": { "type": ["null", "boolean"] }, - "telephone": { - "type": ["null", "string"] + "is_admin": { + "type": ["null", "boolean"] }, - "is_active": { + "is_project_manager": { "type": ["null", "boolean"] }, - "has_access_to_all_future_projects": { + "can_see_rates": { "type": ["null", "boolean"] }, - "default_hourly_rate": { - "type": ["null", "number"] + "can_create_projects": { + "type": ["null", "boolean"] }, - "department": { - "type": ["null", "string"] + "can_create_invoices": { + "type": ["null", "boolean"] }, - "wants_newsletter": { + "is_active": { "type": ["null", "boolean"] }, - "updated_at": { - "type": ["null", "string"], - "format": "date-time" + "weekly_capacity": { + "type": ["null", "integer"] + }, + "default_hourly_rate": { + "type": ["null", "number"] }, "cost_rate": { "type": ["null", "number"] }, - "identity_account_id": { - "type": ["null", "integer"] + "avatar_url": { + "type": ["null", "string"] }, - "indentity_user_id": { - "type": ["null", "integer"] + "created_at": { + "type": ["null", "string"], + "format": "date-time" + }, + "updated_at": { + "type": ["null", "string"], + "format": "date-time" } } }