diff --git a/.travis.yml b/.travis.yml index 323f894c..a19e2818 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: - 2.7 diff --git a/README.md b/README.md deleted file mode 100644 index 70ffb2b7..00000000 --- a/README.md +++ /dev/null @@ -1,442 +0,0 @@ -# python-intercom - -[  ](https://pypi.python.org/pypi/python-intercom) [  ](https://pypi.python.org/pypi/python-intercom) [  ](https://travis-ci.org/jkeyes/python-intercom) [](https://coveralls.io/r/jkeyes/python-intercom?branch=master) - -Python bindings for the Intercom API (https://api.intercom.io). - -[API Documentation](https://api.intercom.io/docs). - -[Package Documentation](http://readthedocs.org/docs/python-intercom/). - -## Upgrading information - -Version 2 of python-intercom is **not backwards compatible** with previous versions. - -One change you will need to make as part of the upgrade is to set `Intercom.app_api_key` and not set `Intercom.api_key`. - -## Installation - - pip install python-intercom - -## Basic Usage - -### Configure your access credentials - -```python -Intercom.app_id = "my_app_id" -Intercom.app_api_key = "my-super-crazy-api-key" -``` - - -### Resources - -Resources this API supports: - - https://api.intercom.io/users - https://api.intercom.io/companies - https://api.intercom.io/tags - https://api.intercom.io/notes - https://api.intercom.io/segments - https://api.intercom.io/events - https://api.intercom.io/conversations - https://api.intercom.io/messages - https://api.intercom.io/counts - https://api.intercom.io/subscriptions - -Additionally, the library can handle incoming webhooks from Intercom and convert to `intercom` models. - -### Examples - - -#### Users - -``` python -from intercom import User -# Find user by email -user = User.find(email="bob@example.com") -# Find user by user_id -user = User.find(user_id="1") -# Find user by id -user = User.find(id="1") -# Create a user -user = User.create(email="bob@example.com", name="Bob Smith") -# Delete a user -deleted_user = User.find(id="1").delete() -# Update custom_attributes for a user -user.custom_attributes["average_monthly_spend"] = 1234.56 -user.save() -# Perform incrementing -user.increment('karma') -user.save() -# Iterate over all users -for user in User.all(): - ... -``` - -#### Admins - -``` python -from intercom import Admin -# Iterate over all admins -for admin in Admin.all(): - ... -``` - -#### Companies - -``` python -from intercom import Company -from intercom import User -# Add a user to one or more companies -user = User.find(email="bob@example.com") -user.companies = [ - {"company_id": 6, "name": "Intercom"}, - {"company_id": 9, "name": "Test Company"} -] -user.save() -# You can also pass custom attributes within a company as you do this -user.companies = [ - { - "id": 6, - "name": "Intercom", - "custom_attributes": { - "referral_source": "Google" - } - } -] -user.save() -# Find a company by company_id -company = Company.find(company_id="44") -# Find a company by name -company = Company.find(name="Some company") -# Find a company by id -company = Company.find(id="41e66f0313708347cb0000d0") -# Update a company -company.name = 'Updated company name' -company.save() -# Iterate over all companies -for company in Company.all(): - ... -# Get a list of users in a company -company.users -``` - -#### Tags - -``` python -from intercom import Tag -# Tag users -tag = Tag.tag_users('blue', ["42ea2f1b93891f6a99000427"]) -# Untag users -Tag.untag_users('blue', ["42ea2f1b93891f6a99000427"]) -# Iterate over all tags -for tag in Tag.all(): - ... -# Iterate over all tags for user -Tag.find_all_for_user(id='53357ddc3c776629e0000029') -Tag.find_all_for_user(email='declan+declan@intercom.io') -Tag.find_all_for_user(user_id='3') -# Tag companies -tag = Tag.tag_companies('red', ["42ea2f1b93891f6a99000427"]) -# Untag companies -Tag.untag_companies('blue', ["42ea2f1b93891f6a99000427"]) -# Iterate over all tags for company -Tag.find_all_for_company(id='43357e2c3c77661e25000026') -Tag.find_all_for_company(company_id='6') -``` - -#### Segments - -``` python -from intercom import Segment -# Find a segment -segment = Segment.find(id=segment_id) -# Update a segment -segment.name = 'Updated name' -segment.save() -# Iterate over all segments -for segment in Segment.all(): - ... -``` - -#### Notes - -``` python -# Find a note by id -note = Note.find(id=note) -# Create a note for a user -note = Note.create( - body="
Text for the note
", - email='joe@example.com') -# Iterate over all notes for a user via their email address -for note in Note.find_all(email='joe@example.com'): - ... -# Iterate over all notes for a user via their user_id -for note in Note.find_all(user_id='123'): - ... -``` - -#### Conversations - -``` python -from intercom import Conversation -# FINDING CONVERSATIONS FOR AN ADMIN -# Iterate over all conversations (open and closed) assigned to an admin -for convo in Conversation.find_all(type='admin', id='7'): - ... -# Iterate over all open conversations assigned to an admin -for convo Conversation.find_all(type='admin', id=7, open=True): - ... -# Iterate over closed conversations assigned to an admin -for convo Conversation.find_all(type='admin', id=7, open=False): - ... -# Iterate over closed conversations for assigned an admin, before a certain -# moment in time -for convo in Conversation.find_all( - type='admin', id= 7, open= False, before=1374844930): - ... - -# FINDING CONVERSATIONS FOR A USER -# Iterate over all conversations (read + unread, correct) with a user based on -# the users email -for convo in Conversation.find_all(email='joe@example.com',type='user'): - ... -# Iterate over through all conversations (read + unread) with a user based on -# the users email -for convo in Conversation.find_all( - email='joe@example.com', type='user', unread=False): - ... -# Iterate over all unread conversations with a user based on the users email -for convo in Conversation.find_all( - email='joe@example.com', type='user', unread=true): - ... - -# FINDING A SINGLE CONVERSATION -conversation = Conversation.find(id='1') - -# INTERACTING WITH THE PARTS OF A CONVERSATION -# Getting the subject of a part (only applies to email-based conversations) -conversation.rendered_message.subject -# Get the part_type of the first part -conversation.conversation_parts[0].part_type -# Get the body of the second part -conversation.conversation_parts[1].body - -# REPLYING TO CONVERSATIONS -# User (identified by email) replies with a comment -conversation.reply( - type='user', email='joe@example.com', - message_type= comment', body='foo') -# Admin (identified by email) replies with a comment -conversation.reply( - type='admin', email='bob@example.com', - message_type='comment', body='bar') -# Admin (identified by id) opens a conversation -conversation.open_conversation(admin_id=7) -# Admin (identified by id) closes a conversation -conversation.close_conversation(admin_id=7) -# Admin (identified by id) assigns a conversation to an assignee -conversation.assign(assignee_id=8, admin_id=7) - -# MARKING A CONVERSATION AS READ -conversation.read = True -conversation.save() -``` - -#### Counts - -``` python -from intercom import Count -# Get Conversation per Admin -conversation_counts_for_each_admin = Count.conversation_counts_for_each_admin() -for count in conversation_counts_for_each_admin: - print "Admin: %s (id: %s) Open: %s Closed: %s" % ( - count.name, count.id, count.open, count.closed) -# Get User Tag Count Object -Count.user_counts_for_each_tag() -# Get User Segment Count Object -Count.user_counts_for_each_segment() -# Get Company Segment Count Object -Count.company_counts_for_each_segment() -# Get Company Tag Count Object -Count.company_counts_for_each_tag() -# Get Company User Count Object -Count.company_counts_for_each_user() -# Get total count of companies, users, segments or tags across app -Company.count() -User.count() -Segment.count() -Tag.count() -``` - -#### Full loading of and embedded entity - -``` python - # Given a converation with a partial user, load the full user. This can be done for any entity - conversation.user.load() -``` - -#### Sending messages - -``` python -# InApp message from admin to user -Message.create(**{ - "message_type": "inapp", - "body": "What's up :)", - "from": { - "type": "admin", - "id": "1234" - }, - "to": { - "type": "user", - "id": "5678" - } -}) - -# Email message from admin to user -Message.create(**{ - "message_type": "email", - "subject": "Hey there", - "body": "What's up :)", - "template": "plain", # or "personal", - "from": { - "type": "admin", - "id": "1234" - }, - "to": { - "type": "user", - "id": "536e564f316c83104c000020" - } -}) - -# Message from a user -Message.create(**{ - "from": { - "type": "user", - "id": "536e564f316c83104c000020" - }, - "body": "halp" -}) -``` - -#### Events - -``` python -from intercom import Event -Event.create( - event_name="invited-friend", - created_at=time.mktime(), - email=user.email, - metadata={ - "invitee_email": "pi@example.org", - "invite_code": "ADDAFRIEND", - "found_date": 12909364407 - } -) -``` - -Metadata Objects support a few simple types that Intercom can present on your behalf - -``` python -Event.create( - event_name="placed-order", - email=current_user.email, - created_at=1403001013 - metadata={ - "order_date": time.mktime(), - "stripe_invoice": 'inv_3434343434', - "order_number": { - "value": '3434-3434', - "url": 'https://example.org/orders/3434-3434' - }, - "price": { - "currency": 'usd', - "amount": 2999 - } - } -) -``` - -The metadata key values in the example are treated as follows- -- order_date: a Date (key ends with '_date'). -- stripe_invoice: The identifier of the Stripe invoice (has a 'stripe_invoice' key) -- order_number: a Rich Link (value contains 'url' and 'value' keys) -- price: An Amount in US Dollars (value contains 'amount' and 'currency' keys) - -### Subscriptions - -Subscribe to events in Intercom to receive webhooks. - -``` python -from intercom import Subscription -# create a subscription -Subscription.create(url="http://example.com", topics=["user.created"]) - -# fetch a subscription -Subscription.find(id="nsub_123456789") - -# list subscriptions -Subscription.all(): -``` - -### Webhooks - -``` python -from intercom import Notification -# create a payload from the notification hash (from json). -payload = Intercom::Notification.new(notification_hash) - -payload.type -# 'user.created' - -payload.model_type -# User - -user = payload.model -# Instance of User -``` - -Note that models generated from webhook notifications might differ slightly from models directly acquired via the API. If this presents a problem, calling `payload.load` will load the model from the API using the `id` field. - - -### Errors - -You do not need to deal with the HTTP response from an API call directly. If there is an unsuccessful response then an error that is a subclass of `intercom.Error` will be raised. If desired, you can get at the http_code of an `Error` via it's `http_code` method. - -The list of different error subclasses are listed below. As they all inherit off `IntercomError` you can choose to except `IntercomError` or the more specific error subclass: - -```python -AuthenticationError -ServerError -ServiceUnavailableError -ResourceNotFound -BadGatewayError -BadRequestError -RateLimitExceeded -MultipleMatchingUsersError -HttpError -UnexpectedError -``` - -### Rate Limiting - -Calling `Intercom.rate_limit_details` returns a dict that contains details about your app's current rate limit. - -```python -Intercom.rate_limit_details -# {'limit': 500, 'reset_at': datetime.datetime(2015, 3, 28, 13, 22), 'remaining': 497} -``` - -## Running the Tests - -Unit tests: - -```bash -nosetests tests/unit -``` - -Integration tests: - -```bash -INTERCOM_APP_ID=xxx INTERCOM_APP_API_KEY=xxx nosetests tests/integration -``` diff --git a/README.rst b/README.rst index 82fc989c..3bee8c0c 100644 --- a/README.rst +++ b/README.rst @@ -13,11 +13,11 @@ DocumentationText for the note
", email='joe@example.com') # Iterate over all notes for a user via their email address - for note in Note.find_all(email='joe@example.com'): + for note in intercom.notes.find_all(email='joe@example.com'): ... # Iterate over all notes for a user via their user_id - for note in Note.find_all(user_id='123'): + for note in intercom.notes.find_all(user_id='123'): ... Conversations @@ -200,40 +194,39 @@ Conversations .. code:: python - from intercom import Conversation # FINDING CONVERSATIONS FOR AN ADMIN # Iterate over all conversations (open and closed) assigned to an admin - for convo in Conversation.find_all(type='admin', id='7'): + for convo in intercom.conversations.find_all(type='admin', id='7'): ... # Iterate over all open conversations assigned to an admin - for convo Conversation.find_all(type='admin', id=7, open=True): + for convo in intercom.conversations.find_all(type='admin', id=7, open=True): ... # Iterate over closed conversations assigned to an admin - for convo Conversation.find_all(type='admin', id=7, open=False): + for convo intercom.conversations.find_all(type='admin', id=7, open=False): ... # Iterate over closed conversations for assigned an admin, before a certain # moment in time - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( type='admin', id= 7, open= False, before=1374844930): ... # FINDING CONVERSATIONS FOR A USER # Iterate over all conversations (read + unread, correct) with a user based on # the users email - for convo in Conversation.find_all(email='joe@example.com',type='user'): + for convo in intercom.onversations.find_all(email='joe@example.com',type='user'): ... # Iterate over through all conversations (read + unread) with a user based on # the users email - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( email='joe@example.com', type='user', unread=False): ... # Iterate over all unread conversations with a user based on the users email - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( email='joe@example.com', type='user', unread=true): ... # FINDING A SINGLE CONVERSATION - conversation = Conversation.find(id='1') + conversation = intercom.conversations.find(id='1') # INTERACTING WITH THE PARTS OF A CONVERSATION # Getting the subject of a part (only applies to email-based conversations) @@ -245,52 +238,45 @@ Conversations # REPLYING TO CONVERSATIONS # User (identified by email) replies with a comment - conversation.reply( + intercom.conversations.reply( type='user', email='joe@example.com', - message_type= comment', body='foo') + message_type='comment', body='foo') # Admin (identified by email) replies with a comment - conversation.reply( + intercom.conversations.reply( type='admin', email='bob@example.com', message_type='comment', body='bar') + # User (identified by email) replies with a comment and attachment + intercom.conversations.reply(id=conversation.id, type='user', email='joe@example.com', message_type='comment', body='foo', attachment_urls=['http://www.example.com/attachment.jpg']) - # MARKING A CONVERSATION AS READ - conversation.read = True - conversation.save() + # Open + intercom.conversations.open(id=conversation.id, admin_id='123') -Counts -^^^^^^ + # Close + intercom.conversations.close(id=conversation.id, admin_id='123') -.. code:: python + # Assign + intercom.conversations.assign(id=conversation.id, admin_id='123', assignee_id='124') + + # Reply and Open + intercom.conversations.reply(id=conversation.id, type='admin', admin_id='123', message_type='open', body='bar') + + # Reply and Close + intercom.conversations.reply(id=conversation.id, type='admin', admin_id='123', message_type='close', body='bar') - from intercom import Count - # Get Conversation per Admin - conversation_counts_for_each_admin = Count.conversation_counts_for_each_admin() - for count in conversation_counts_for_each_admin: - print "Admin: %s (id: %s) Open: %s Closed: %s" % ( - count.name, count.id, count.open, count.closed) - # Get User Tag Count Object - Count.user_counts_for_each_tag() - # Get User Segment Count Object - Count.user_counts_for_each_segment() - # Get Company Segment Count Object - Count.company_counts_for_each_segment() - # Get Company Tag Count Object - Count.company_counts_for_each_tag() - # Get Company User Count Object - Count.company_counts_for_each_user() - # Get total count of companies, users, segments or tags across app - Company.count() - User.count() - Segment.count() - Tag.count() - -Full loading of and embedded entity -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + # ASSIGNING CONVERSATIONS TO ADMINS + intercom.conversations.reply(id=conversation.id, type='admin', assignee_id=assignee_admin.id, admin_id=admin.id, message_type='assignment') + + # MARKING A CONVERSATION AS READ + intercom.conversations.mark_read(conversation.id) + +Full loading of an embedded entity +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code:: python - # Given a converation with a partial user, load the full user. This can be done for any entity - conversation.user.load() + # Given a conversation with a partial user, load the full user. This can be + # done for any entity + intercom.users.load(conversation.user) Sending messages ^^^^^^^^^^^^^^^^ @@ -298,7 +284,7 @@ Sending messages .. code:: python # InApp message from admin to user - Message.create(**{ + intercom.messages.create(**{ "message_type": "inapp", "body": "What's up :)", "from": { @@ -312,7 +298,7 @@ Sending messages }) # Email message from admin to user - Message.create(**{ + intercom.messages.create(**{ "message_type": "email", "subject": "Hey there", "body": "What's up :)", @@ -328,7 +314,7 @@ Sending messages }) # Message from a user - Message.create(**{ + intercom.messages.create(**{ "from": { "type": "user", "id": "536e564f316c83104c000020" @@ -336,20 +322,41 @@ Sending messages "body": "halp" }) + # Message from admin to contact + intercom.messages.create(**{ + 'body': 'How can I help :)', + 'from': { + 'type': 'admin', + 'id': '1234' + }, + 'to': { + 'type': 'contact', + 'id': '536e5643as316c83104c400671' + } + }) + + # Message from a contact + intercom.messages.create(**{ + 'from' => { + 'type': 'contact', + 'id': '536e5643as316c83104c400671' + }, + 'body': 'halp' + }) + Events ^^^^^^ .. code:: python - from intercom import Event - Event.create( - event_name="invited-friend", + intercom.events.create( + event_name='invited-friend', created_at=time.mktime(), email=user.email, metadata={ - "invitee_email": "pi@example.org", - "invite_code": "ADDAFRIEND", - "found_date": 12909364407 + 'invitee_email': 'pi@example.org', + 'invite_code': 'ADDAFRIEND', + 'found_date': 12909364407 } ) @@ -358,20 +365,20 @@ your behalf .. code:: python - Event.create( + intercom.events.create( event_name="placed-order", email=current_user.email, created_at=1403001013 metadata={ - "order_date": time.mktime(), - "stripe_invoice": 'inv_3434343434', - "order_number": { - "value": '3434-3434', - "url": 'https://example.org/orders/3434-3434' + 'order_date': time.mktime(), + 'stripe_invoice': 'inv_3434343434', + 'order_number': { + 'value': '3434-3434', + 'url': 'https://example.org/orders/3434-3434' }, - "price": { - "currency": 'usd', - "amount": 2999 + 'price': { + 'currency': 'usd', + 'amount': 2999 } } ) @@ -385,6 +392,88 @@ The metadata key values in the example are treated as follows- - price: An Amount in US Dollars (value contains 'amount' and 'currency' keys) +Bulk operations. + +.. code:: python + + # Submit bulk job, to create events + intercom.events.submit_bulk_job(create_items: [ + { + 'event_name': 'ordered-item', + 'created_at': 1438944980, + 'user_id': '314159', + 'metadata': { + 'order_date': 1438944980, + 'stripe_invoice': 'inv_3434343434' + } + }, + { + 'event_name': 'invited-friend', + 'created_at': 1438944979, + 'user_id': '314159', + 'metadata': { + 'invitee_email': 'pi@example.org', + 'invite_code': 'ADDAFRIEND' + } + } + ]) + + # Submit bulk job, to add items to existing job + intercom.events.submit_bulk_job(create_items=[ + { + 'event_name': 'ordered-item', + 'created_at': 1438944980, + 'user_id': '314159', + 'metadata': { + 'order_date': 1438944980, + 'stripe_invoice': 'inv_3434343434' + } + }, + { + 'event_name': 'invited-friend', + 'created_at': 1438944979, + 'user_id': "314159", + 'metadata': { + 'invitee_email': 'pi@example.org', + 'invite_code': 'ADDAFRIEND' + } + } + ], job_id='job_abcd1234') + +Contacts +^^^^^^^^ + +Contacts represent logged out users of your application. + +.. code:: python + + # Create a contact + contact = intercom.contacts.create(email="some_contact@example.com") + + # Update a contact + contact.custom_attributes['foo'] = 'bar' + intercom.contacts.save(contact) + + # Find contacts by email + contacts = intercom.contacts.find_all(email="some_contact@example.com") + + # Convert a contact into a user + intercom.contacts.convert(contact, user) + + # Delete a contact + intercom.contacts.delete(contact) + +Counts +^^^^^^ + +.. code:: python + + # App-wide counts + intercom.counts.for_app + + # Users in segment counts + intercom.counts.for_type(type='user', count='segment') + Subscriptions ~~~~~~~~~~~~~ @@ -392,38 +481,26 @@ Subscribe to events in Intercom to receive webhooks. .. code:: python - from intercom import Subscription # create a subscription - Subscription.create(url="http://example.com", topics=["user.created"]) + intercom.subscriptions.create(url='http://example.com', topics=['user.created']) # fetch a subscription - Subscription.find(id="nsub_123456789") + intercom.subscriptions.find(id='nsub_123456789') # list subscriptions - Subscription.all(): + intercom.subscriptions.all(): + ... -Webhooks -~~~~~~~~ +Bulk jobs +^^^^^^^^^ .. code:: python - from intercom import Notification - # create a payload from the notification hash (from json). - payload = Intercom::Notification.new(notification_hash) - - payload.type - # 'user.created' - - payload.model_type - # User - - user = payload.model - # Instance of User + # fetch a job + intercom.jobs.find(id='job_abcd1234') -Note that models generated from webhook notifications might differ -slightly from models directly acquired via the API. If this presents a -problem, calling ``payload.load`` will load the model from the API using -the ``id`` field. + # fetch a job's error feed + intercom.jobs.errors(id='job_abcd1234') Errors ~~~~~~ @@ -442,6 +519,7 @@ or the more specific error subclass: AuthenticationError ServerError ServiceUnavailableError + ServiceConnectionError ResourceNotFound BadGatewayError BadRequestError @@ -453,13 +531,13 @@ or the more specific error subclass: Rate Limiting ~~~~~~~~~~~~~ -Calling ``Intercom.rate_limit_details`` returns a dict that contains +Calling your clients ``rate_limit_details`` returns a dict that contains details about your app's current rate limit. .. code:: python - Intercom.rate_limit_details - # {'limit': 500, 'reset_at': datetime.datetime(2015, 3, 28, 13, 22), 'remaining': 497} + intercom.rate_limit_details + # {'limit': 180, 'remaining': 179, 'reset_at': datetime.datetime(2014, 10, 07, 14, 58)} Running the Tests ----------------- diff --git a/intercom/__init__.py b/intercom/__init__.py index c7ef2e61..0cb56385 100644 --- a/intercom/__init__.py +++ b/intercom/__init__.py @@ -1,32 +1,12 @@ # -*- coding: utf-8 -*- -from datetime import datetime +# from datetime import datetime from .errors import (ArgumentError, AuthenticationError, # noqa BadGatewayError, BadRequestError, HttpError, IntercomError, MultipleMatchingUsersError, RateLimitExceeded, ResourceNotFound, ServerError, ServiceUnavailableError, UnexpectedError) -from .lib.setter_property import SetterProperty -from .request import Request -from .admin import Admin # noqa -from .company import Company # noqa -from .count import Count # noqa -from .conversation import Conversation # noqa -from .event import Event # noqa -from .message import Message # noqa -from .note import Note # noqa -from .notification import Notification # noqa -from .user import User # noqa -from .segment import Segment # noqa -from .subscription import Subscription # noqa -from .tag import Tag # noqa -import copy -import random -import re -import six -import time - -__version__ = '2.1.1' +__version__ = '3.0b2' RELATED_DOCS_TEXT = "See https://github.com/jkeyes/python-intercom \ @@ -38,142 +18,3 @@ Intercom.app_api_key and don't set Intercom.api_key." CONFIGURATION_REQUIRED_TEXT = "You must set both Intercom.app_id and \ Intercom.app_api_key to use this client." - - -class IntercomType(type): # noqa - - app_id = None - app_api_key = None - _hostname = "api.intercom.io" - _protocol = "https" - _endpoints = None - _current_endpoint = None - _target_base_url = None - _endpoint_randomized_at = 0 - _rate_limit_details = {} - - @property - def _auth(self): - return (self.app_id, self.app_api_key) - - @property - def _random_endpoint(self): - if self.endpoints: - endpoints = copy.copy(self.endpoints) - random.shuffle(endpoints) - return endpoints[0] - - @property - def _alternative_random_endpoint(self): - endpoints = copy.copy(self.endpoints) - if self.current_endpoint in endpoints: - endpoints.remove(self.current_endpoint) - random.shuffle(endpoints) - if endpoints: - return endpoints[0] - - @property - def target_base_url(self): - if None in [self.app_id, self.app_api_key]: - raise ArgumentError('%s %s' % ( - CONFIGURATION_REQUIRED_TEXT, RELATED_DOCS_TEXT)) - if self._target_base_url is None: - basic_auth_part = '%s:%s@' % (self.app_id, self.app_api_key) - if self.current_endpoint: - self._target_base_url = re.sub( - r'(https?:\/\/)(.*)', - '\g<1>%s\g<2>' % (basic_auth_part), - self.current_endpoint) - return self._target_base_url - - @property - def hostname(self): - return self._hostname - - @hostname.setter - def hostname(self, value): - self._hostname = value - self.current_endpoint = None - self.endpoints = None - - @property - def rate_limit_details(self): - return self._rate_limit_details - - @rate_limit_details.setter - def rate_limit_details(self, value): - self._rate_limit_details = value - - @property - def protocol(self): - return self._protocol - - @protocol.setter - def protocol(self, value): - self._protocol = value - self.current_endpoint = None - self.endpoints = None - - @property - def current_endpoint(self): - now = time.mktime(datetime.utcnow().timetuple()) - expired = self._endpoint_randomized_at < (now - (60 * 5)) - if self._endpoint_randomized_at is None or expired: - self._endpoint_randomized_at = now - self._current_endpoint = self._random_endpoint - return self._current_endpoint - - @current_endpoint.setter - def current_endpoint(self, value): - self._current_endpoint = value - self._target_base_url = None - - @property - def endpoints(self): - if not self._endpoints: - return ['%s://%s' % (self.protocol, self.hostname)] - else: - return self._endpoints - - @endpoints.setter - def endpoints(self, value): - self._endpoints = value - self.current_endpoint = self._random_endpoint - - @SetterProperty - def endpoint(self, value): - self.endpoints = [value] - - -@six.add_metaclass(IntercomType) -class Intercom(object): - _class_register = {} - - @classmethod - def get_url(cls, path): - if '://' in path: - url = path - else: - url = cls.current_endpoint + path - return url - - @classmethod - def request(cls, method, path, params): - return Request.send_request_to_path( - method, cls.get_url(path), cls._auth, params) - - @classmethod - def get(cls, path, **params): - return cls.request('GET', path, params) - - @classmethod - def post(cls, path, **params): - return cls.request('POST', path, params) - - @classmethod - def put(cls, path, **params): - return cls.request('PUT', path, params) - - @classmethod - def delete(cls, path, **params): - return cls.request('DELETE', path, params) diff --git a/intercom/admin.py b/intercom/admin.py index 646838e2..8879d892 100644 --- a/intercom/admin.py +++ b/intercom/admin.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.all import All -from intercom.api_operations.find import Find from intercom.traits.api_resource import Resource -class Admin(Resource, Find, All): +class Admin(Resource): pass diff --git a/intercom/api_operations/all.py b/intercom/api_operations/all.py index 64d9fd94..04a5ab73 100644 --- a/intercom/api_operations/all.py +++ b/intercom/api_operations/all.py @@ -6,8 +6,9 @@ class All(object): - @classmethod - def all(cls): - collection = utils.resource_class_to_collection_name(cls) + def all(self): + collection = utils.resource_class_to_collection_name( + self.collection_class) finder_url = "/%s" % (collection) - return CollectionProxy(cls, collection, finder_url) + return CollectionProxy( + self.client, self.collection_class, collection, finder_url) diff --git a/intercom/api_operations/bulk.py b/intercom/api_operations/bulk.py new file mode 100644 index 00000000..7681ceb8 --- /dev/null +++ b/intercom/api_operations/bulk.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Support for the Intercom Bulk API. + +Ref: https://developers.intercom.io/reference#bulk-apis +""" + +from intercom import utils + + +def item_for_api(method, data_type, item): + """Return a Bulk API item.""" + return { + 'method': method, + 'data_type': data_type, + 'data': item + } + + +class Submit(object): + """Provide Bulk API support to subclasses.""" + + def submit_bulk_job(self, create_items=[], delete_items=[], job_id=None): + """Submit a Bulk API job.""" + from intercom import event + from intercom.errors import HttpError + from intercom.job import Job + + if self.collection_class == event.Event and delete_items: + raise Exception("Events do not support bulk delete operations.") + data_type = utils.resource_class_to_name(self.collection_class) + collection_name = utils.resource_class_to_collection_name(self.collection_class) + create_items = [item_for_api('post', data_type, item) for item in create_items] + delete_items = [item_for_api('delete', data_type, item) for item in delete_items] + + bulk_request = { + 'items': create_items + delete_items + } + if job_id: + bulk_request['job'] = {'id': job_id} + + response = self.client.post('/bulk/%s' % (collection_name), bulk_request) + if not response: + raise HttpError('HTTP Error - No response entity returned.') + return Job().from_response(response) + + +class LoadErrorFeed(object): + """Provide access to Bulk API error feed for a specific job.""" + + def errors(self, id): + """Return errors for the Bulk API job specified.""" + from intercom.errors import HttpError + from intercom.job import Job + response = self.client.get("/jobs/%s/error" % (id), {}) + if not response: + raise HttpError('Http Error - No response entity returned.') + return Job.from_api(response) diff --git a/intercom/api_operations/convert.py b/intercom/api_operations/convert.py new file mode 100644 index 00000000..43e2ac98 --- /dev/null +++ b/intercom/api_operations/convert.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + + +class Convert(object): + + def convert(self, contact, user): + self.client.post( + '/contacts/convert', + { + 'contact': {'user_id': contact.user_id}, + 'user': self.identity_hash(user) + } + ) diff --git a/intercom/api_operations/delete.py b/intercom/api_operations/delete.py index 5bc33f44..68a9501d 100644 --- a/intercom/api_operations/delete.py +++ b/intercom/api_operations/delete.py @@ -5,8 +5,8 @@ class Delete(object): - def delete(self): - from intercom import Intercom - collection = utils.resource_class_to_collection_name(self.__class__) - Intercom.delete("/%s/%s/" % (collection, self.id)) - return self + def delete(self, obj): + collection = utils.resource_class_to_collection_name( + self.collection_class) + self.client.delete("/%s/%s" % (collection, obj.id), {}) + return obj diff --git a/intercom/api_operations/find.py b/intercom/api_operations/find.py index f1ba7886..986d692a 100644 --- a/intercom/api_operations/find.py +++ b/intercom/api_operations/find.py @@ -6,16 +6,16 @@ class Find(object): - @classmethod - def find(cls, **params): - from intercom import Intercom - collection = utils.resource_class_to_collection_name(cls) + def find(self, **params): + collection = utils.resource_class_to_collection_name( + self.collection_class) if 'id' in params: - response = Intercom.get("/%s/%s" % (collection, params['id'])) + response = self.client.get( + "/%s/%s" % (collection, params['id']), {}) else: - response = Intercom.get("/%s" % (collection), **params) + response = self.client.get("/%s" % (collection), params) if response is None: raise HttpError('Http Error - No response entity returned') - return cls(**response) + return self.collection_class(**response) diff --git a/intercom/api_operations/find_all.py b/intercom/api_operations/find_all.py index 0f8687c4..d0933724 100644 --- a/intercom/api_operations/find_all.py +++ b/intercom/api_operations/find_all.py @@ -6,12 +6,14 @@ class FindAll(object): - @classmethod - def find_all(cls, **params): - collection = utils.resource_class_to_collection_name(cls) + def find_all(self, **params): + collection = utils.resource_class_to_collection_name( + self.collection_class) if 'id' in params and 'type' not in params: finder_url = "/%s/%s" % (collection, params['id']) else: finder_url = "/%s" % (collection) finder_params = params - return CollectionProxy(cls, collection, finder_url, finder_params) + return CollectionProxy( + self.client, self.collection_class, collection, + finder_url, finder_params) diff --git a/intercom/api_operations/load.py b/intercom/api_operations/load.py index 9662e8ce..266113ca 100644 --- a/intercom/api_operations/load.py +++ b/intercom/api_operations/load.py @@ -6,17 +6,17 @@ class Load(object): - def load(self): - from intercom import Intercom - cls = self.__class__ - collection = utils.resource_class_to_collection_name(cls) - if hasattr(self, 'id'): - response = Intercom.get("/%s/%s" % (collection, self.id)) + def load(self, resource): + collection = utils.resource_class_to_collection_name( + self.collection_class) + if hasattr(resource, 'id'): + response = self.client.get("/%s/%s" % (collection, resource.id), {}) # noqa else: raise Exception( - "Cannot load %s as it does not have a valid id." % (cls)) + "Cannot load %s as it does not have a valid id." % ( + self.collection_class)) if response is None: raise HttpError('Http Error - No response entity returned') - return cls(**response) + return resource.from_response(response) diff --git a/intercom/api_operations/save.py b/intercom/api_operations/save.py index 61872e9c..b90a0ea3 100644 --- a/intercom/api_operations/save.py +++ b/intercom/api_operations/save.py @@ -5,65 +5,61 @@ class Save(object): - @classmethod - def create(cls, **params): - from intercom import Intercom - collection = utils.resource_class_to_collection_name(cls) - response = Intercom.post("/%s/" % (collection), **params) + def create(self, **params): + collection = utils.resource_class_to_collection_name( + self.collection_class) + response = self.client.post("/%s/" % (collection), params) if response: # may be empty if we received a 202 - return cls(**response) + return self.collection_class(**response) - def from_dict(self, pdict): - for key, value in list(pdict.items()): - setattr(self, key, value) + # def from_dict(self, pdict): + # for key, value in list(pdict.items()): + # setattr(self, key, value) - @property - def to_dict(self): - a_dict = {} - for name in list(self.__dict__.keys()): - if name == "changed_attributes": - continue - a_dict[name] = self.__dict__[name] # direct access - return a_dict + # @property + # def to_dict(self): + # a_dict = {} + # for name in list(self.__dict__.keys()): + # if name == "changed_attributes": + # continue + # a_dict[name] = self.__dict__[name] # direct access + # return a_dict - @classmethod - def from_api(cls, response): - obj = cls() - obj.from_response(response) - return obj + # @classmethod + # def from_api(cls, response): + # obj = cls() + # obj.from_response(response) + # return obj - def from_response(self, response): - self.from_dict(response) - return self + # def from_response(self, response): + # self.from_dict(response) + # return self - def save(self): - from intercom import Intercom - collection = utils.resource_class_to_collection_name(self.__class__) - params = self.attributes - if self.id_present and not self.posted_updates: + def save(self, obj): + collection = utils.resource_class_to_collection_name( + obj.__class__) + params = obj.attributes + if self.id_present(obj) and not self.posted_updates(obj): # update - response = Intercom.put('/%s/%s' % (collection, self.id), **params) + response = self.client.put('/%s/%s' % (collection, obj.id), params) else: # create - params.update(self.identity_hash) - response = Intercom.post('/%s' % (collection), **params) + params.update(self.identity_hash(obj)) + response = self.client.post('/%s' % (collection), params) if response: - return self.from_response(response) + return obj.from_response(response) - @property - def id_present(self): - return getattr(self, 'id', None) and self.id != "" + def id_present(self, obj): + return getattr(obj, 'id', None) and obj.id != "" - @property - def posted_updates(self): - return getattr(self, 'update_verb', None) == 'post' + def posted_updates(self, obj): + return getattr(obj, 'update_verb', None) == 'post' - @property - def identity_hash(self): - identity_vars = getattr(self, 'identity_vars', []) + def identity_hash(self, obj): + identity_vars = getattr(obj, 'identity_vars', []) parts = {} for var in identity_vars: - id_var = getattr(self, var, None) + id_var = getattr(obj, var, None) if id_var: # only present id var if it is not blank or None parts[var] = id_var return parts diff --git a/intercom/client.py b/intercom/client.py new file mode 100644 index 00000000..42c575c6 --- /dev/null +++ b/intercom/client.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + + +class Client(object): + + def __init__(self, app_id='my_app_id', api_key='my_api_key'): + self.app_id = app_id + self.api_key = api_key + self.base_url = 'https://api.intercom.io' + self.rate_limit_details = {} + + @property + def _auth(self): + return (self.app_id, self.api_key) + + @property + def admins(self): + from intercom.service import admin + return admin.Admin(self) + + @property + def companies(self): + from intercom.service import company + return company.Company(self) + + @property + def conversations(self): + from intercom.service import conversation + return conversation.Conversation(self) + + @property + def counts(self): + from intercom.service import count + return count.Count(self) + + @property + def events(self): + from intercom.service import event + return event.Event(self) + + @property + def messages(self): + from intercom.service import message + return message.Message(self) + + @property + def notes(self): + from intercom.service import note + return note.Note(self) + + @property + def segments(self): + from intercom.service import segment + return segment.Segment(self) + + @property + def subscriptions(self): + from intercom.service import subscription + return subscription.Subscription(self) + + @property + def tags(self): + from intercom.service import tag + return tag.Tag(self) + + @property + def users(self): + from intercom.service import user + return user.User(self) + + @property + def leads(self): + from intercom.service import lead + return lead.Lead(self) + + @property + def jobs(self): + from intercom.service import job + return job.Job(self) + + def _execute_request(self, request, params): + result = request.execute(self.base_url, self._auth, params) + self.rate_limit_details = request.rate_limit_details + return result + + def get(self, path, params): + from intercom import request + req = request.Request('GET', path) + return self._execute_request(req, params) + + def post(self, path, params): + from intercom import request + req = request.Request('POST', path) + return self._execute_request(req, params) + + def put(self, path, params): + from intercom import request + req = request.Request('PUT', path) + return self._execute_request(req, params) + + def delete(self, path, params): + from intercom import request + req = request.Request('DELETE', path) + return self._execute_request(req, params) diff --git a/intercom/collection_proxy.py b/intercom/collection_proxy.py index b86e0d20..62d27469 100644 --- a/intercom/collection_proxy.py +++ b/intercom/collection_proxy.py @@ -2,13 +2,25 @@ import six from intercom import HttpError +from intercom import utils class CollectionProxy(six.Iterator): - def __init__(self, cls, collection, finder_url, finder_params={}): + def __init__( + self, client, collection_cls, collection, + finder_url, finder_params={}): + + self.client = client + + # resource name + self.resource_name = utils.resource_class_to_collection_name(collection_cls) + + # resource class + self.resource_class = collection_cls + # needed to create class instances of the resource - self.collection_cls = cls + self.collection_cls = collection_cls # needed to reference the collection in the response self.collection = collection @@ -60,13 +72,13 @@ def get_next_page(self): def get_page(self, url, params={}): # get a page of results - from intercom import Intercom + # from intercom import Intercom # if there is no url stop iterating if url is None: raise StopIteration - response = Intercom.get(url, **params) + response = self.client.get(url, params) if response is None: raise HttpError('Http Error - No response entity returned') diff --git a/intercom/company.py b/intercom/company.py index efd40889..9c34d085 100644 --- a/intercom/company.py +++ b/intercom/company.py @@ -1,16 +1,9 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.all import All -from intercom.api_operations.count import Count -from intercom.api_operations.delete import Delete -from intercom.api_operations.find import Find -from intercom.api_operations.load import Load -from intercom.api_operations.save import Save -from intercom.extended_api_operations.users import Users from intercom.traits.api_resource import Resource -class Company(Resource, Delete, Count, Find, All, Save, Load, Users): +class Company(Resource): update_verb = 'post' identity_vars = ['id', 'company_id'] diff --git a/intercom/conversation.py b/intercom/conversation.py index 7a8780a5..99d0a5ee 100644 --- a/intercom/conversation.py +++ b/intercom/conversation.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.find_all import FindAll -from intercom.api_operations.find import Find -from intercom.api_operations.load import Load -from intercom.api_operations.save import Save -from intercom.extended_api_operations.reply import Reply from intercom.traits.api_resource import Resource -class Conversation(Resource, FindAll, Find, Load, Save, Reply): +class Conversation(Resource): pass diff --git a/intercom/count.py b/intercom/count.py index acb335c2..43dc5e63 100644 --- a/intercom/count.py +++ b/intercom/count.py @@ -1,26 +1,20 @@ # -*- coding: utf-8 -*- -import six - -from intercom.api_operations.find import Find -from intercom.generic_handlers.count import Counter -from intercom.generic_handlers.base_handler import BaseHandler -from intercom.api_operations.count import Count as CountOperation from intercom.traits.api_resource import Resource -@six.add_metaclass(BaseHandler) -class Count(Resource, Find, CountOperation, Counter): +class Count(Resource): + pass - @classmethod - def fetch_for_app(cls): - return Count.find() + # @classmethod + # def fetch_for_app(cls): + # return Count.find() - @classmethod - def do_broken_down_count(cls, entity_to_count, count_context): - result = cls.fetch_broken_down_count(entity_to_count, count_context) - return getattr(result, entity_to_count)[count_context] + # @classmethod + # def do_broken_down_count(cls, entity_to_count, count_context): + # result = cls.fetch_broken_down_count(entity_to_count, count_context) + # return getattr(result, entity_to_count)[count_context] - @classmethod - def fetch_broken_down_count(cls, entity_to_count, count_context): - return Count.find(type=entity_to_count, count=count_context) + # @classmethod + # def fetch_broken_down_count(cls, entity_to_count, count_context): + # return Count.find(type=entity_to_count, count=count_context) diff --git a/intercom/errors.py b/intercom/errors.py index 589aa709..cf34b0d5 100644 --- a/intercom/errors.py +++ b/intercom/errors.py @@ -57,6 +57,7 @@ class UnexpectedError(IntercomError): 'unauthorized': AuthenticationError, 'forbidden': AuthenticationError, 'bad_request': BadRequestError, + 'action_forbidden': BadRequestError, 'missing_parameter': BadRequestError, 'parameter_invalid': BadRequestError, 'parameter_not_found': BadRequestError, diff --git a/intercom/event.py b/intercom/event.py index ee05f2b2..0eb1fb69 100644 --- a/intercom/event.py +++ b/intercom/event.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.find import Find -from intercom.api_operations.save import Save from intercom.traits.api_resource import Resource -class Event(Resource, Save, Find): +class Event(Resource): pass diff --git a/intercom/events.py b/intercom/events.py deleted file mode 100644 index 971816fa..00000000 --- a/intercom/events.py +++ /dev/null @@ -1,40 +0,0 @@ -# coding=utf-8 -# -# Copyright 2014 martin@mekkaoui.fr -# -# License: MIT -# -""" Intercom API wrapper. """ - -from . import Intercom -from .user import UserId - - -class Event(UserId): - - @classmethod - def create(cls, event_name=None, user_id=None, email=None, metadata=None): - resp = Intercom.create_event(event_name=event_name, user_id=user_id, email=email, metadata=metadata) - return Event(resp) - - def save(self): - """ Create an Event from this objects properties: - - >>> event = Event() - >>> event.event_name = "shared-item" - >>> event.email = "joe@example.com" - >>> event.save() - - """ - resp = Intercom.create_event(**self) - self.update(resp) - - @property - def event_name(self): - """ The name of the Event. """ - return dict.get(self, 'event_name', None) - - @event_name.setter - def event_name(self, event_name): - """ Set the event name. """ - self['event_name'] = event_name diff --git a/intercom/extended_api_operations/users.py b/intercom/extended_api_operations/users.py index 39e0dcef..33a2a44f 100644 --- a/intercom/extended_api_operations/users.py +++ b/intercom/extended_api_operations/users.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- from intercom import utils -from intercom.user import User from intercom.collection_proxy import CollectionProxy class Users(object): - @property - def users(self): - collection = utils.resource_class_to_collection_name(self.__class__) - finder_url = "/%s/%s/users" % (collection, self.id) - return CollectionProxy(User, "users", finder_url) + def users(self, id): + collection = utils.resource_class_to_collection_name( + self.collection_class) + finder_url = "/%s/%s/users" % (collection, id) + return CollectionProxy( + self.client, self.collection_class, "users", finder_url) diff --git a/intercom/job.py b/intercom/job.py new file mode 100644 index 00000000..d501a0b1 --- /dev/null +++ b/intercom/job.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- # noqa + +from intercom.traits.api_resource import Resource + + +class Job(Resource): + """A Bulk API Job. + + Ref: https://developers.intercom.io/reference#bulk-job-model + """ diff --git a/intercom/lead.py b/intercom/lead.py new file mode 100644 index 00000000..815e3732 --- /dev/null +++ b/intercom/lead.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from intercom.traits.api_resource import Resource + + +class Lead(Resource): + + update_verb = 'put' + identity_vars = ['email', 'user_id'] + collection_name = 'contacts' + + @property + def flat_store_attributes(self): + return ['custom_attributes'] diff --git a/intercom/message.py b/intercom/message.py index 4a0d38d0..3d84ef97 100644 --- a/intercom/message.py +++ b/intercom/message.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.save import Save from intercom.traits.api_resource import Resource -class Message(Resource, Save): +class Message(Resource): pass diff --git a/intercom/note.py b/intercom/note.py index 59a56499..f903cdb6 100644 --- a/intercom/note.py +++ b/intercom/note.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.find_all import FindAll -from intercom.api_operations.find import Find -from intercom.api_operations.save import Save -from intercom.api_operations.load import Load from intercom.traits.api_resource import Resource -class Note(Resource, Find, FindAll, Load, Save): +class Note(Resource): pass diff --git a/intercom/request.py b/intercom/request.py index c4002d5c..dfca9925 100644 --- a/intercom/request.py +++ b/intercom/request.py @@ -15,35 +15,45 @@ class Request(object): timeout = 10 - @classmethod - def send_request_to_path(cls, method, url, auth, params=None): + def __init__(self, http_method, path): + self.http_method = http_method + self.path = path + + def execute(self, base_url, auth, params): + return self.send_request_to_path(base_url, auth, params) + + def send_request_to_path(self, base_url, auth, params=None): """ Construct an API request, send it to the API, and parse the response. """ from intercom import __version__ req_params = {} + # full URL + url = base_url + self.path + headers = { 'User-Agent': 'python-intercom/' + __version__, + 'AcceptEncoding': 'gzip, deflate', 'Accept': 'application/json' } - if method in ('POST', 'PUT', 'DELETE'): + if self.http_method in ('POST', 'PUT', 'DELETE'): headers['content-type'] = 'application/json' req_params['data'] = json.dumps(params, cls=ResourceEncoder) - elif method == 'GET': + elif self.http_method == 'GET': req_params['params'] = params req_params['headers'] = headers # request logging if logger.isEnabledFor(logging.DEBUG): - logger.debug("Sending %s request to: %s", method, url) + logger.debug("Sending %s request to: %s", self.http_method, url) logger.debug(" headers: %s", headers) - if method == 'GET': + if self.http_method == 'GET': logger.debug(" params: %s", req_params['params']) else: logger.debug(" params: %s", req_params['data']) resp = requests.request( - method, url, timeout=cls.timeout, + self.http_method, url, timeout=self.timeout, auth=auth, verify=certifi.where(), **req_params) # response logging @@ -53,29 +63,26 @@ def send_request_to_path(cls, method, url, auth, params=None): resp.encoding, resp.status_code) logger.debug(" content:\n%s", resp.content) - cls.raise_errors_on_failure(resp) - cls.set_rate_limit_details(resp) + parsed_body = self.parse_body(resp) + self.raise_errors_on_failure(resp) + self.set_rate_limit_details(resp) + return parsed_body + def parse_body(self, resp): if resp.content and resp.content.strip(): - # parse non empty bodies - return cls.parse_body(resp) - - @classmethod - def parse_body(cls, resp): - try: - # use supplied or inferred encoding to decode the - # response content - decoded_body = resp.content.decode( - resp.encoding or resp.apparent_encoding) - body = json.loads(decoded_body) - if body.get('type') == 'error.list': - cls.raise_application_errors_on_failure(body, resp.status_code) - return body - except ValueError: - cls.raise_errors_on_failure(resp) - - @classmethod - def set_rate_limit_details(cls, resp): + try: + # use supplied or inferred encoding to decode the + # response content + decoded_body = resp.content.decode( + resp.encoding or resp.apparent_encoding) + body = json.loads(decoded_body) + if body.get('type') == 'error.list': + self.raise_application_errors_on_failure(body, resp.status_code) # noqa + return body + except ValueError: + self.raise_errors_on_failure(resp) + + def set_rate_limit_details(self, resp): rate_limit_details = {} headers = resp.headers limit = headers.get('x-ratelimit-limit', None) @@ -87,11 +94,9 @@ def set_rate_limit_details(cls, resp): rate_limit_details['remaining'] = int(remaining) if reset: rate_limit_details['reset_at'] = datetime.fromtimestamp(int(reset)) - from intercom import Intercom - Intercom.rate_limit_details = rate_limit_details + self.rate_limit_details = rate_limit_details - @classmethod - def raise_errors_on_failure(cls, resp): + def raise_errors_on_failure(self, resp): if resp.status_code == 404: raise errors.ResourceNotFound('Resource Not Found') elif resp.status_code == 401: @@ -105,8 +110,7 @@ def raise_errors_on_failure(cls, resp): elif resp.status_code == 503: raise errors.ServiceUnavailableError('Service Unavailable') - @classmethod - def raise_application_errors_on_failure(cls, error_list_details, http_code): # noqa + def raise_application_errors_on_failure(self, error_list_details, http_code): # noqa # Currently, we don't support multiple errors error_details = error_list_details['errors'][0] error_code = error_details.get('type') @@ -120,24 +124,22 @@ def raise_application_errors_on_failure(cls, error_list_details, http_code): # if error_class is None: # unexpected error if error_code: - message = cls.message_for_unexpected_error_with_type( + message = self.message_for_unexpected_error_with_type( error_details, http_code) else: - message = cls.message_for_unexpected_error_without_type( + message = self.message_for_unexpected_error_without_type( error_details, http_code) error_class = errors.UnexpectedError else: message = error_details.get('message') raise error_class(message, error_context) - @classmethod - def message_for_unexpected_error_with_type(cls, error_details, http_code): # noqa + def message_for_unexpected_error_with_type(self, error_details, http_code): # noqa error_type = error_details.get('type') message = error_details.get('message') return "The error of type '%s' is not recognized. It occurred with the message: %s and http_code: '%s'. Please contact Intercom with these details." % (error_type, message, http_code) # noqa - @classmethod - def message_for_unexpected_error_without_type(cls, error_details, http_code): # noqa + def message_for_unexpected_error_without_type(self, error_details, http_code): # noqa message = error_details['message'] return "An unexpected error occured. It occurred with the message: %s and http_code: '%s'. Please contact Intercom with these details." % (message, http_code) # noqa diff --git a/intercom/segment.py b/intercom/segment.py index 1c3d3d39..72a95bd5 100644 --- a/intercom/segment.py +++ b/intercom/segment.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.all import All -from intercom.api_operations.count import Count -from intercom.api_operations.find import Find -from intercom.api_operations.save import Save from intercom.traits.api_resource import Resource -class Segment(Resource, Find, Count, Save, All): +class Segment(Resource): pass diff --git a/intercom/service/__init__.py b/intercom/service/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/intercom/service/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/intercom/service/admin.py b/intercom/service/admin.py new file mode 100644 index 00000000..ad8d9b02 --- /dev/null +++ b/intercom/service/admin.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from intercom import admin +from intercom.api_operations.all import All +from intercom.service.base_service import BaseService + + +class Admin(BaseService, All): + + @property + def collection_class(self): + return admin.Admin diff --git a/intercom/service/base_service.py b/intercom/service/base_service.py new file mode 100644 index 00000000..c299e181 --- /dev/null +++ b/intercom/service/base_service.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + + +class BaseService(object): + + def __init__(self, client): + self.client = client + + @property + def collection_class(self): + raise NotImplementedError + + def from_api(self, api_response): + obj = self.collection_class() + obj.from_response(api_response) + return obj diff --git a/intercom/service/company.py b/intercom/service/company.py new file mode 100644 index 00000000..4dbd384b --- /dev/null +++ b/intercom/service/company.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +from intercom import company +from intercom.api_operations.all import All +from intercom.api_operations.delete import Delete +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.extended_api_operations.users import Users +from intercom.service.base_service import BaseService + + +class Company(BaseService, All, Delete, Find, FindAll, Save, Load, Users): + + @property + def collection_class(self): + return company.Company + +# require 'intercom/extended_api_operations/tags' +# require 'intercom/extended_api_operations/segments' diff --git a/intercom/service/conversation.py b/intercom/service/conversation.py new file mode 100644 index 00000000..3b3ac6cd --- /dev/null +++ b/intercom/service/conversation.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +from intercom import conversation +from intercom import utils +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.service.base_service import BaseService + + +class Conversation(BaseService, Find, FindAll, Save, Load): + + @property + def collection_class(self): + return conversation.Conversation + + def reply(self, **reply_data): + return self.__reply(reply_data) + + def assign(self, **reply_data): + reply_data['type'] = 'admin' + reply_data['message_type'] = 'assignment' + return self.__reply(reply_data) + + def open(self, **reply_data): + reply_data['type'] = 'admin' + reply_data['message_type'] = 'open' + return self.__reply(reply_data) + + def close(self, **reply_data): + reply_data['type'] = 'admin' + reply_data['message_type'] = 'close' + return self.__reply(reply_data) + + def __reply(self, reply_data): + _id = reply_data.pop('id') + collection = utils.resource_class_to_collection_name(self.collection_class) # noqa + url = "/%s/%s/reply" % (collection, _id) + reply_data['conversation_id'] = _id + response = self.client.post(url, reply_data) + return self.collection_class().from_response(response) + + +# def mark_read(id) +# @client.put("/conversations/#{id}", read: true) +# end diff --git a/intercom/service/count.py b/intercom/service/count.py new file mode 100644 index 00000000..11b93082 --- /dev/null +++ b/intercom/service/count.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from intercom import count +from intercom.api_operations.find import Find +from intercom.service.base_service import BaseService + + +class Count(BaseService, Find): + + @property + def collection_class(self): + return count.Count + + def for_app(self): + return self.find() + + def for_type(self, type, count=None): + return self.find(type=type, count=count) + + # @classmethod + # def do_broken_down_count(cls, entity_to_count, count_context): + # result = cls.fetch_broken_down_count(entity_to_count, count_context) + # return getattr(result, entity_to_count)[count_context] + + # @classmethod + # def fetch_broken_down_count(cls, entity_to_count, count_context): + # return Count.find(type=entity_to_count, count=count_context) diff --git a/intercom/service/event.py b/intercom/service/event.py new file mode 100644 index 00000000..2243e6fe --- /dev/null +++ b/intercom/service/event.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from intercom import event +from intercom.api_operations.bulk import Submit +from intercom.api_operations.save import Save +from intercom.service.base_service import BaseService + + +class Event(BaseService, Save, Submit): + + @property + def collection_class(self): + return event.Event diff --git a/intercom/service/job.py b/intercom/service/job.py new file mode 100644 index 00000000..0dcda25c --- /dev/null +++ b/intercom/service/job.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from intercom import job +from intercom.api_operations.all import All +from intercom.api_operations.bulk import LoadErrorFeed +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.service.base_service import BaseService + + +class Job(BaseService, All, Find, FindAll, Save, Load, LoadErrorFeed): + + @property + def collection_class(self): + return job.Job diff --git a/intercom/service/lead.py b/intercom/service/lead.py new file mode 100644 index 00000000..b1da78bc --- /dev/null +++ b/intercom/service/lead.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- # noqa + +from intercom import lead +from intercom.api_operations.all import All +from intercom.api_operations.convert import Convert +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.delete import Delete +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.service.base_service import BaseService + + +class Lead(BaseService, All, Find, FindAll, Delete, Save, Load, Convert): + """Leads are useful for representing logged-out users of your application. + + Ref: https://developers.intercom.io/reference#leads + """ + + @property + def collection_class(self): + """The collection class that represents this resource.""" + return lead.Lead diff --git a/intercom/service/message.py b/intercom/service/message.py new file mode 100644 index 00000000..d9c29451 --- /dev/null +++ b/intercom/service/message.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- + +from intercom import message +from intercom.api_operations.save import Save +from intercom.service.base_service import BaseService + + +class Message(BaseService, Save): + + @property + def collection_class(self): + return message.Message diff --git a/intercom/service/note.py b/intercom/service/note.py new file mode 100644 index 00000000..eaaf4f0b --- /dev/null +++ b/intercom/service/note.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from intercom import note +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.service.base_service import BaseService + + +class Note(BaseService, Find, FindAll, Save, Load): + + @property + def collection_class(self): + return note.Note diff --git a/intercom/service/segment.py b/intercom/service/segment.py new file mode 100644 index 00000000..79c3b7e9 --- /dev/null +++ b/intercom/service/segment.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from intercom import segment +from intercom.api_operations.all import All +from intercom.api_operations.find import Find +from intercom.service.base_service import BaseService + + +class Segment(BaseService, All, Find): + + @property + def collection_class(self): + return segment.Segment diff --git a/intercom/service/subscription.py b/intercom/service/subscription.py new file mode 100644 index 00000000..31f3e56b --- /dev/null +++ b/intercom/service/subscription.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from intercom import subscription +from intercom.api_operations.all import All +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.api_operations.delete import Delete +from intercom.service.base_service import BaseService + + +class Subscription(BaseService, All, Find, FindAll, Save, Delete): + + @property + def collection_class(self): + return subscription.Subscription diff --git a/intercom/service/tag.py b/intercom/service/tag.py new file mode 100644 index 00000000..1bf1a5ea --- /dev/null +++ b/intercom/service/tag.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +from intercom import tag +from intercom.api_operations.all import All +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.save import Save +from intercom.service.base_service import BaseService + + +class Tag(BaseService, All, Find, FindAll, Save): + + @property + def collection_class(self): + return tag.Tag + + def tag(self, **params): + params['tag_or_untag'] = 'tag' + return self.create(**params) + + def untag(self, **params): + params['tag_or_untag'] = 'untag' + for user_or_company in self._users_or_companies(params): + user_or_company['untag'] = True + return self.create(**params) + + def _users_or_companies(self, params): + if 'users' in params: + return params['users'] + if 'companies' in params: + return params['companies'] + return [] diff --git a/intercom/service/user.py b/intercom/service/user.py new file mode 100644 index 00000000..e219f2b3 --- /dev/null +++ b/intercom/service/user.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from intercom import user +from intercom.api_operations.all import All +from intercom.api_operations.bulk import Submit +from intercom.api_operations.find import Find +from intercom.api_operations.find_all import FindAll +from intercom.api_operations.delete import Delete +from intercom.api_operations.save import Save +from intercom.api_operations.load import Load +from intercom.service.base_service import BaseService + + +class User(BaseService, All, Find, FindAll, Delete, Save, Load, Submit): + + @property + def collection_class(self): + return user.User diff --git a/intercom/subscription.py b/intercom/subscription.py index f93056e1..640d0757 100644 --- a/intercom/subscription.py +++ b/intercom/subscription.py @@ -1,11 +1,7 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.find import Find -from intercom.api_operations.delete import Delete -from intercom.api_operations.find_all import FindAll -from intercom.api_operations.save import Save from intercom.traits.api_resource import Resource -class Subscription(Resource, Find, FindAll, Save, Delete): +class Subscription(Resource): pass diff --git a/intercom/tag.py b/intercom/tag.py index fd59dcd9..3a12bd99 100644 --- a/intercom/tag.py +++ b/intercom/tag.py @@ -1,18 +1,7 @@ # -*- coding: utf-8 -*- -import six - -from intercom.api_operations.all import All -from intercom.api_operations.count import Count -from intercom.api_operations.find import Find -from intercom.api_operations.find_all import FindAll -from intercom.api_operations.save import Save -from intercom.generic_handlers.base_handler import BaseHandler -from intercom.generic_handlers.tag import TagUntag -from intercom.generic_handlers.tag_find_all import TagFindAll from intercom.traits.api_resource import Resource -@six.add_metaclass(BaseHandler) -class Tag(Resource, All, Count, Find, FindAll, Save, TagUntag, TagFindAll): +class Tag(Resource): pass diff --git a/intercom/traits/api_resource.py b/intercom/traits/api_resource.py index af4090f3..524efbc7 100644 --- a/intercom/traits/api_resource.py +++ b/intercom/traits/api_resource.py @@ -33,9 +33,13 @@ def to_datetime_value(value): class Resource(object): + client = None changed_attributes = [] - def __init__(_self, **params): # noqa + def __init__(_self, *args, **params): # noqa + if args: + _self.client = args[0] + # intercom includes a 'self' field in the JSON, to avoid the naming # conflict we go with _self here _self.from_dict(params) @@ -69,6 +73,14 @@ def from_dict(self, dict): # already exists in Intercom self.changed_attributes = [] + def to_dict(self): + a_dict = {} + for name in list(self.__dict__.keys()): + if name == "changed_attributes": + continue + a_dict[name] = self.__dict__[name] # direct access + return a_dict + @property def attributes(self): res = {} diff --git a/intercom/user.py b/intercom/user.py index f57f533a..a5629238 100644 --- a/intercom/user.py +++ b/intercom/user.py @@ -1,21 +1,13 @@ # -*- coding: utf-8 -*- -from intercom.api_operations.all import All -from intercom.api_operations.count import Count -from intercom.api_operations.delete import Delete -from intercom.api_operations.find import Find -from intercom.api_operations.find_all import FindAll -from intercom.api_operations.load import Load -from intercom.api_operations.save import Save from intercom.traits.api_resource import Resource from intercom.traits.incrementable_attributes import IncrementableAttributes -class User(Resource, Find, FindAll, All, Count, Load, Save, Delete, - IncrementableAttributes): +class User(Resource, IncrementableAttributes): update_verb = 'post' - identity_vars = ['email', 'user_id'] + identity_vars = ['id', 'email', 'user_id'] @property def flat_store_attributes(self): diff --git a/intercom/utils.py b/intercom/utils.py index 4319339b..0350f873 100644 --- a/intercom/utils.py +++ b/intercom/utils.py @@ -25,6 +25,8 @@ def constantize_singular_resource_name(resource_name): def resource_class_to_collection_name(cls): + if hasattr(cls, 'collection_name'): + return cls.collection_name return pluralize(cls.__name__.lower()) diff --git a/setup.py b/setup.py index 7af6c2de..9fa5e18b 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ with open(os.path.join('intercom', '__init__.py')) as init: source = init.read() - m = re.search("__version__ = '(\d+\.\d+(\.(\d+|[a-z]+))?)'", source, re.M) + m = re.search("__version__ = '(.*)'", source, re.M) __version__ = m.groups()[0] with open('README.rst') as readme: @@ -28,7 +28,12 @@ license="MIT License", url="http://github.com/jkeyes/python-intercom", keywords='Intercom crm python', - classifiers=[], + classifiers=[ + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + ], packages=find_packages(), include_package_data=True, install_requires=["requests", "inflection", "certifi", "six"], diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index c0bea9eb..8db6f1aa 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -3,9 +3,9 @@ import time from datetime import datetime -from intercom import Company +# from intercom import Company from intercom import ResourceNotFound -from intercom import User +# from intercom import User def get_timestamp(): @@ -13,14 +13,14 @@ def get_timestamp(): return int(time.mktime(now.timetuple())) -def get_or_create_user(timestamp): +def get_or_create_user(client, timestamp): # get user email = '%s@example.com' % (timestamp) try: - user = User.find(email=email) + user = client.users.find(email=email) except ResourceNotFound: # Create a user - user = User.create( + user = client.users.create( email=email, user_id=timestamp, name="Ada %s" % (timestamp)) @@ -28,22 +28,30 @@ def get_or_create_user(timestamp): return user -def get_or_create_company(timestamp): +def get_or_create_company(client, timestamp): name = 'Company %s' % (timestamp) # get company try: - company = Company.find(name=name) + company = client.companies.find(name=name) except ResourceNotFound: # Create a company - company = Company.create( + company = client.companies.create( company_id=timestamp, name=name) return company -def delete(resource): +def delete_user(client, resource): try: - resource.delete() + client.users.delete(resource) + except ResourceNotFound: + # not much we can do here + pass + + +def delete_company(client, resource): + try: + client.companies.delete(resource) except ResourceNotFound: # not much we can do here pass diff --git a/tests/integration/issues/test_72.py b/tests/integration/issues/test_72.py index c576cae0..5fa95983 100644 --- a/tests/integration/issues/test_72.py +++ b/tests/integration/issues/test_72.py @@ -3,22 +3,22 @@ import os import unittest import time -from intercom import Intercom -from intercom import Event -from intercom import User -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +from intercom.client import Client + +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class Issue72Test(unittest.TestCase): def test(self): - User.create(email='me@example.com') + intercom.users.create(email='me@example.com') # no exception here as empty response expected data = { 'event_name': 'Eventful 1', 'created_at': int(time.time()), 'email': 'me@example.com' } - Event.create(**data) + intercom.events.create(**data) diff --git a/tests/integration/issues/test_73.py b/tests/integration/issues/test_73.py index cc5ce90c..b2fa1a81 100644 --- a/tests/integration/issues/test_73.py +++ b/tests/integration/issues/test_73.py @@ -5,30 +5,31 @@ import os import unittest -from intercom import Intercom -from intercom import User -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +from intercom.client import Client + +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class Issue73Test(unittest.TestCase): def test(self): - user = User.create(email='bingo@example.com') + user = intercom.users.create(email='bingo@example.com') # store current session count session_count = user.session_count # register a new session user.new_session = True - user.save() + intercom.users.save(user) # count has increased by 1 self.assertEquals(session_count + 1, user.session_count) # register a new session user.new_session = True - user.save() + intercom.users.save(user) # count has increased by 1 self.assertEquals(session_count + 2, user.session_count) diff --git a/tests/integration/test_admin.py b/tests/integration/test_admin.py index 22d59381..4b6e57cc 100644 --- a/tests/integration/test_admin.py +++ b/tests/integration/test_admin.py @@ -2,17 +2,17 @@ import os import unittest -from intercom import Intercom -from intercom import Admin +from intercom.client import Client -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class AdminTest(unittest.TestCase): def test(self): # Iterate over all admins - for admin in Admin.all(): + for admin in intercom.admins.all(): self.assertIsNotNone(admin.id) self.assertIsNotNone(admin.email) diff --git a/tests/integration/test_company.py b/tests/integration/test_company.py index e7e285b1..f0f8c5da 100644 --- a/tests/integration/test_company.py +++ b/tests/integration/test_company.py @@ -2,16 +2,16 @@ import os import unittest -from intercom import Company -from intercom import Intercom -from intercom import User -from . import delete +from intercom.client import Client +from . import delete_company +from . import delete_user from . import get_or_create_user from . import get_or_create_company from . import get_timestamp -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class CompanyTest(unittest.TestCase): @@ -19,27 +19,27 @@ class CompanyTest(unittest.TestCase): @classmethod def setup_class(cls): nowstamp = get_timestamp() - cls.company = get_or_create_company(nowstamp) - cls.user = get_or_create_user(nowstamp) + cls.company = get_or_create_company(intercom, nowstamp) + cls.user = get_or_create_user(intercom, nowstamp) @classmethod def teardown_class(cls): - delete(cls.company) - delete(cls.user) + delete_company(intercom, cls.company) + delete_user(intercom, cls.user) def test_add_user(self): - user = User.find(email=self.user.email) + user = intercom.users.find(email=self.user.email) user.companies = [ {"company_id": 6, "name": "Intercom"}, {"company_id": 9, "name": "Test Company"} ] - user.save() - user = User.find(email=self.user.email) + intercom.users.save(user) + user = intercom.users.find(email=self.user.email) self.assertEqual(len(user.companies), 2) self.assertEqual(user.companies[0].company_id, "9") def test_add_user_custom_attributes(self): - user = User.find(email=self.user.email) + user = intercom.users.find(email=self.user.email) user.companies = [ { "id": 6, @@ -49,49 +49,49 @@ def test_add_user_custom_attributes(self): } } ] - user.save() - user = User.find(email=self.user.email) + intercom.users.save(user) + user = intercom.users.find(email=self.user.email) self.assertEqual(len(user.companies), 2) self.assertEqual(user.companies[0].company_id, "9") # check the custom attributes - company = Company.find(company_id=6) + company = intercom.companies.find(company_id=6) self.assertEqual( company.custom_attributes['referral_source'], "Google") def test_find_by_company_id(self): # Find a company by company_id - company = Company.find(company_id=self.company.company_id) + company = intercom.companies.find(company_id=self.company.company_id) self.assertEqual(company.company_id, self.company.company_id) def test_find_by_company_name(self): # Find a company by name - company = Company.find(name=self.company.name) + company = intercom.companies.find(name=self.company.name) self.assertEqual(company.name, self.company.name) def test_find_by_id(self): # Find a company by _id - company = Company.find(id=self.company.id) + company = intercom.companies.find(id=self.company.id) self.assertEqual(company.company_id, self.company.company_id) def test_update(self): # Find a company by id - company = Company.find(id=self.company.id) + company = intercom.companies.find(id=self.company.id) # Update a company now = get_timestamp() updated_name = 'Company %s' % (now) company.name = updated_name - company.save() - company = Company.find(id=self.company.id) + intercom.companies.save(company) + company = intercom.companies.find(id=self.company.id) self.assertEqual(company.name, updated_name) def test_iterate(self): # Iterate over all companies - for company in Company.all(): + for company in intercom.companies.all(): self.assertTrue(company.id is not None) def test_users(self): - company = Company.find(id=self.company.id) + company = intercom.companies.find(id=self.company.id) # Get a list of users in a company - for user in company.users: + for user in intercom.companies.users(company.id): self.assertIsNotNone(user.email) diff --git a/tests/integration/test_conversations.py b/tests/integration/test_conversations.py index 0a5655cb..54e51159 100644 --- a/tests/integration/test_conversations.py +++ b/tests/integration/test_conversations.py @@ -2,27 +2,28 @@ import os import unittest -from intercom import Intercom -from intercom import Admin -from intercom import Conversation -from intercom import Message -from . import delete +from intercom.client import Client +# from intercom import Admin +# from intercom import Conversation +# from intercom import Message +from . import delete_user from . import get_or_create_user from . import get_timestamp -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class ConversationTest(unittest.TestCase): @classmethod def setup_class(cls): # get admin - cls.admin = Admin.all()[1] + cls.admin = intercom.admins.all()[1] # get user timestamp = get_timestamp() - cls.user = get_or_create_user(timestamp) + cls.user = get_or_create_user(intercom, timestamp) cls.email = cls.user.email # send user message @@ -33,40 +34,41 @@ def setup_class(cls): }, 'body': "Hey" } - cls.user_message = Message.create(**message_data) + cls.user_message = intercom.messages.create(**message_data) - conversations = Conversation.find_all() - user_init_conv = conversations[0] + conversations = intercom.conversations.find_all() + cls.user_init_conv = conversations[0] # send admin reply - cls.admin_conv = user_init_conv.reply( + cls.admin_conv = intercom.conversations.reply( + id=cls.user_init_conv.id, type='admin', admin_id=cls.admin.id, message_type='comment', body='There') @classmethod def teardown_class(cls): - delete(cls.user) + delete_user(intercom, cls.user) def test_find_all_admin(self): # FINDING CONVERSATIONS FOR AN ADMIN # Iterate over all conversations (open and closed) assigned to an admin - for convo in Conversation.find_all(type='admin', id=self.admin.id): + for convo in intercom.conversations.find_all(type='admin', id=self.admin.id): # noqa self.assertIsNotNone(convo.id) self.admin_conv.id = convo.id def test_find_all_open_admin(self): # Iterate over all open conversations assigned to an admin - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( type='admin', id=self.admin.id, open=True): self.assertIsNotNone(convo.id) def test_find_all_closed_admin(self): # Iterate over closed conversations assigned to an admin - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( type='admin', id=self.admin.id, open=False): self.assertIsNotNone(convo.id) def test_find_all_closed_before_admin(self): - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( type='admin', id=self.admin.id, open=False, before=1374844930): self.assertIsNotNone(convo.id) @@ -75,33 +77,33 @@ def test_find_all_user(self): # FINDING CONVERSATIONS FOR A USER # Iterate over all conversations (read + unread, correct) with a # user based on the users email - for convo in Conversation.find_all(email=self.email, type='user'): + for convo in intercom.conversations.find_all(email=self.email, type='user'): # noqa self.assertIsNotNone(convo.id) def test_find_all_read(self): # Iterate over through all conversations (read + unread) with a # user based on the users email - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( email=self.email, type='user', unread=False): self.assertIsNotNone(convo.id) def test_find_all_unread(self): # Iterate over all unread conversations with a user based on the # users email - for convo in Conversation.find_all( + for convo in intercom.conversations.find_all( email=self.email, type='user', unread=True): self.assertIsNotNone(convo.id) def test_find_single_conversation(self): # FINDING A SINGLE CONVERSATION - convo_id = Conversation.find_all(type='admin', id=self.admin.id)[0].id - conversation = Conversation.find(id=convo_id) + convo_id = intercom.conversations.find_all(type='admin', id=self.admin.id)[0].id # noqa + conversation = intercom.conversations.find(id=convo_id) self.assertEqual(conversation.id, convo_id) def test_conversation_parts(self): # INTERACTING WITH THE PARTS OF A CONVERSATION - convo_id = Conversation.find_all(type='admin', id=self.admin.id)[0].id - conversation = Conversation.find(id=convo_id) + convo_id = intercom.conversations.find_all(type='admin', id=self.admin.id)[0].id # noqa + conversation = intercom.conversations.find(id=convo_id) # Getting the subject of a part (only applies to email-based # conversations) @@ -113,55 +115,62 @@ def test_conversation_parts(self): if not part.part_type == 'assignment': self.assertIsNotNone(part.body) - def test_reply(self): + def test_a_reply(self): # REPLYING TO CONVERSATIONS - conversation = Conversation.find(id=self.admin_conv.id) + conversation = intercom.conversations.find(id=self.admin_conv.id) num_parts = len(conversation.conversation_parts) # User (identified by email) replies with a comment - conversation.reply( + intercom.conversations.reply( + id=conversation.id, type='user', email=self.email, message_type='comment', body='foo') # Admin (identified by admin_id) replies with a comment - conversation.reply( + intercom.conversations.reply( + id=conversation.id, type='admin', admin_id=self.admin.id, message_type='comment', body='bar') - conversation = Conversation.find(id=self.admin_conv.id) + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertEqual(num_parts + 2, len(conversation.conversation_parts)) def test_open(self): # OPENING CONVERSATIONS - conversation = Conversation.find(id=self.admin_conv.id) - conversation.close_conversation(admin_id=self.admin.id, body='Closing message') + conversation = intercom.conversations.find(id=self.admin_conv.id) + intercom.conversations.close( + id=conversation.id, admin_id=self.admin.id, body='Closing message') # noqa self.assertFalse(conversation.open) - conversation.open_conversation(admin_id=self.admin.id, body='Opening message') - conversation = Conversation.find(id=self.admin_conv.id) + intercom.conversations.open( + id=conversation.id, admin_id=self.admin.id, body='Opening message') # noqa + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertTrue(conversation.open) def test_close(self): # CLOSING CONVERSATIONS - conversation = Conversation.find(id=self.admin_conv.id) + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertTrue(conversation.open) - conversation.close_conversation(admin_id=self.admin.id, body='Closing message') - conversation = Conversation.find(id=self.admin_conv.id) + intercom.conversations.close( + id=conversation.id, admin_id=self.admin.id, body='Closing message') # noqa + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertFalse(conversation.open) def test_assignment(self): # ASSIGNING CONVERSATIONS - conversation = Conversation.find(id=self.admin_conv.id) + conversation = intercom.conversations.find(id=self.admin_conv.id) num_parts = len(conversation.conversation_parts) - conversation.assign(assignee_id=self.admin.id, admin_id=self.admin.id) - conversation = Conversation.find(id=self.admin_conv.id) + intercom.conversations.assign( + id=conversation.id, assignee_id=self.admin.id, + admin_id=self.admin.id) + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertEqual(num_parts + 1, len(conversation.conversation_parts)) - self.assertEqual("assignment", conversation.conversation_parts[-1].part_type) + self.assertEqual("assignment", conversation.conversation_parts[-1].part_type) # noqa def test_mark_read(self): # MARKING A CONVERSATION AS READ - conversation = Conversation.find(id=self.admin_conv.id) + conversation = intercom.conversations.find(id=self.admin_conv.id) conversation.read = False - conversation.save() - conversation = Conversation.find(id=self.admin_conv.id) + intercom.conversations.save(conversation) + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertFalse(conversation.read) conversation.read = True - conversation.save() - conversation = Conversation.find(id=self.admin_conv.id) + intercom.conversations.save(conversation) + conversation = intercom.conversations.find(id=self.admin_conv.id) self.assertTrue(conversation.read) diff --git a/tests/integration/test_count.py b/tests/integration/test_count.py index 806c33cd..5bff2417 100644 --- a/tests/integration/test_count.py +++ b/tests/integration/test_count.py @@ -31,7 +31,6 @@ def setup_class(cls): def teardown_class(cls): delete(cls.company) delete(cls.user) - print(Intercom.rate_limit_details) def test_user_counts_for_each_tag(self): # Get User Tag Count Object diff --git a/tests/integration/test_notes.py b/tests/integration/test_notes.py index eba0a03b..ea1d8f6c 100644 --- a/tests/integration/test_notes.py +++ b/tests/integration/test_notes.py @@ -2,57 +2,57 @@ import os import unittest -from intercom import Intercom -from intercom import Note -from . import delete +from intercom.client import Client +from . import delete_user from . import get_or_create_user from . import get_timestamp -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class NoteTest(unittest.TestCase): + @classmethod def setup_class(cls): timestamp = get_timestamp() - cls.user = get_or_create_user(timestamp) + cls.user = get_or_create_user(intercom, timestamp) cls.email = cls.user.email @classmethod def teardown_class(cls): - delete(cls.user) + delete_user(intercom, cls.user) def test_create_note(self): # Create a note for a user - note = Note.create( + note = intercom.notes.create( body="Text for the note
", email=self.email) self.assertIsNotNone(note.id) def test_find_note(self): # Find a note by id - orig_note = Note.create( + orig_note = intercom.notes.create( body="Text for the note
", email=self.email) - note = Note.find(id=orig_note.id) + note = intercom.notes.find(id=orig_note.id) self.assertEqual(note.body, orig_note.body) def test_find_all_email(self): # Iterate over all notes for a user via their email address - notes = Note.find_all(email=self.email) + notes = intercom.notes.find_all(email=self.email) for note in notes: self.assertTrue(note.id is not None) - user = note.user.load() + user = intercom.users.load(note.user) self.assertEqual(user.email, self.email) break def test_find_all_id(self): - from intercom.user import User - user = User.find(email=self.email) + user = intercom.users.find(email=self.email) # Iterate over all notes for a user via their email address - for note in Note.find_all(user_id=user.user_id): + for note in intercom.notes.find_all(user_id=user.user_id): self.assertTrue(note.id is not None) - user = note.user.load() + user = intercom.users.load(note.user) self.assertEqual(user.email, self.email) diff --git a/tests/integration/test_segments.py b/tests/integration/test_segments.py index d9b54f80..edd163c6 100644 --- a/tests/integration/test_segments.py +++ b/tests/integration/test_segments.py @@ -1,38 +1,26 @@ # -*- coding: utf-8 -*- import os -import time import unittest -from datetime import datetime -from intercom import Intercom -from intercom import Segment +from intercom.client import Client -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class SegmentTest(unittest.TestCase): @classmethod def setup_class(cls): - cls.segment = Segment.all()[0] + cls.segment = intercom.segments.all()[0] def test_find_segment(self): # Find a segment - segment = Segment.find(id=self.segment.id) + segment = intercom.segments.find(id=self.segment.id) self.assertEqual(segment.id, self.segment.id) - def test_save_segment(self): - # Update a segment - segment = Segment.find(id=self.segment.id) - now = datetime.utcnow() - updated_name = 'Updated %s' % (time.mktime(now.timetuple())) - segment.name = updated_name - segment.save() - segment = Segment.find(id=self.segment.id) - self.assertEqual(segment.name, updated_name) - def test_iterate(self): # Iterate over all segments - for segment in Segment.all(): + for segment in intercom.segments.all(): self.assertTrue(segment.id is not None) diff --git a/tests/integration/test_tags.py b/tests/integration/test_tags.py index cf7579a5..1ac00042 100644 --- a/tests/integration/test_tags.py +++ b/tests/integration/test_tags.py @@ -2,17 +2,16 @@ import os import unittest -from intercom import Intercom -from intercom import Tag -from intercom import User -from intercom import Company -from . import delete +from intercom.client import Client +from . import delete_user +from . import delete_company from . import get_or_create_company from . import get_or_create_user from . import get_timestamp -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class TagTest(unittest.TestCase): @@ -20,82 +19,49 @@ class TagTest(unittest.TestCase): @classmethod def setup_class(cls): nowstamp = get_timestamp() - cls.company = get_or_create_company(nowstamp) - cls.user = get_or_create_user(nowstamp) + cls.company = get_or_create_company(intercom, nowstamp) + cls.user = get_or_create_user(intercom, nowstamp) cls.user.companies = [ {"company_id": cls.company.id, "name": cls.company.name} ] - cls.user.save() + intercom.users.save(cls.user) @classmethod def teardown_class(cls): - delete(cls.company) - delete(cls.user) + delete_company(intercom, cls.company) + delete_user(intercom, cls.user) def test_tag_users(self): # Tag users - tag = Tag.tag_users('blue', [self.user.id]) + tag = intercom.tags.tag(name='blue', users=[{'id': self.user.id}]) self.assertEqual(tag.name, 'blue') - user = User.find(email=self.user.email) + user = intercom.users.find(email=self.user.email) self.assertEqual(1, len(user.tags)) def test_untag_users(self): # Untag users - tag = Tag.untag_users('blue', [self.user.id]) + tag = intercom.tags.untag(name='blue', users=[{'id': self.user.id}]) self.assertEqual(tag.name, 'blue') - user = User.find(email=self.user.email) + user = intercom.users.find(email=self.user.email) self.assertEqual(0, len(user.tags)) def test_all(self): # Iterate over all tags - for tag in Tag.all(): - self.assertIsNotNone(tag.id) - - def test_all_for_user_by_id(self): - # Iterate over all tags for user - tags = Tag.find_all_for_user(id=self.user.id) - for tag in tags: - self.assertIsNotNone(tag.id) - - def test_all_for_user_by_email(self): - # Iterate over all tags for user - tags = Tag.find_all_for_user(email=self.user.email) - for tag in tags: - self.assertIsNotNone(tag.id) - - def test_all_for_user_by_user_id(self): - # Iterate over all tags for user - tags = Tag.find_all_for_user(user_id=self.user.user_id) - for tag in tags: + for tag in intercom.tags.all(): self.assertIsNotNone(tag.id) def test_tag_companies(self): # Tag companies - tag = Tag.tag_companies("red", [self.user.companies[0].id]) - self.assertEqual(tag.name, "red") - company = Company.find(id=self.user.companies[0].id) + tag = intercom.tags.tag( + name="blue", companies=[{'id': self.user.companies[0].id}]) + self.assertEqual(tag.name, "blue") + company = intercom.companies.find(id=self.user.companies[0].id) self.assertEqual(1, len(company.tags)) def test_untag_companies(self): # Untag companies - tag = Tag.untag_companies("red", [self.user.companies[0].id]) - self.assertEqual(tag.name, "red") - company = Company.find(id=self.user.companies[0].id) + tag = intercom.tags.untag( + name="blue", companies=[{'id': self.user.companies[0].id}]) + self.assertEqual(tag.name, "blue") + company = intercom.companies.find(id=self.user.companies[0].id) self.assertEqual(0, len(company.tags)) - - # Iterate over all tags for company - def test_all_for_company_by_id(self): - # Iterate over all tags for user - red_tag = Tag.tag_companies("red", [self.company.id]) - tags = Tag.find_all_for_company(id=self.company.id) - for tag in tags: - self.assertEqual(red_tag.id, tag.id) - Tag.untag_companies("red", [self.company.id]) - - def test_all_for_company_by_company_id(self): - # Iterate over all tags for user - red_tag = Tag.tag_companies("red", [self.company.id]) - tags = Tag.find_all_for_company(company_id=self.company.id) - for tag in tags: - self.assertEqual(red_tag.id, tag.id) - Tag.untag_companies("red", [self.company.id]) diff --git a/tests/integration/test_user.py b/tests/integration/test_user.py index 73fcd9f2..3ba62b87 100644 --- a/tests/integration/test_user.py +++ b/tests/integration/test_user.py @@ -2,14 +2,14 @@ import os import unittest -from intercom import Intercom -from intercom import User +from intercom.client import Client from . import get_timestamp from . import get_or_create_user -from . import delete +from . import delete_user -Intercom.app_id = os.environ.get('INTERCOM_APP_ID') -Intercom.app_api_key = os.environ.get('INTERCOM_APP_API_KEY') +intercom = Client( + os.environ.get('INTERCOM_APP_ID'), + os.environ.get('INTERCOM_API_KEY')) class UserTest(unittest.TestCase): @@ -17,49 +17,49 @@ class UserTest(unittest.TestCase): @classmethod def setup_class(cls): nowstamp = get_timestamp() - cls.user = get_or_create_user(nowstamp) + cls.user = get_or_create_user(intercom, nowstamp) cls.email = cls.user.email @classmethod def teardown_class(cls): - delete(cls.user) + delete_user(intercom, cls.user) def test_find_by_email(self): # Find user by email - user = User.find(email=self.email) + user = intercom.users.find(email=self.email) self.assertEqual(self.email, user.email) def test_find_by_user_id(self): # Find user by user id - user = User.find(user_id=self.user.user_id) + user = intercom.users.find(user_id=self.user.user_id) self.assertEqual(self.email, user.email) def test_find_by_id(self): # Find user by id - user = User.find(id=self.user.id) + user = intercom.users.find(id=self.user.id) self.assertEqual(self.email, user.email) def test_custom_attributes(self): # Update custom_attributes for a user - user = User.find(id=self.user.id) + user = intercom.users.find(id=self.user.id) user.custom_attributes["average_monthly_spend"] = 1234.56 - user.save() - user = User.find(id=self.user.id) + intercom.users.save(user) + user = intercom.users.find(id=self.user.id) self.assertEqual( user.custom_attributes["average_monthly_spend"], 1234.56) def test_increment(self): # Perform incrementing - user = User.find(id=self.user.id) + user = intercom.users.find(id=self.user.id) karma = user.custom_attributes.get('karma', 0) user.increment('karma') - user.save() + intercom.users.save(user) self.assertEqual(user.custom_attributes["karma"], karma + 1) user.increment('karma') - user.save() + intercom.users.save(user) self.assertEqual(user.custom_attributes["karma"], karma + 2) def test_iterate(self): # Iterate over all users - for user in User.all(): + for user in intercom.users.all(): self.assertTrue(user.id is not None) diff --git a/tests/unit/test_admin.py b/tests/unit/test_admin.py index a22e550b..63eb0c9f 100644 --- a/tests/unit/test_admin.py +++ b/tests/unit/test_admin.py @@ -2,8 +2,8 @@ import unittest -from intercom import Request -from intercom import Admin +from intercom.request import Request +from intercom.client import Client from intercom.collection_proxy import CollectionProxy from mock import patch from nose.tools import assert_raises @@ -20,8 +20,9 @@ class AdminTest(unittest.TestCase): @istest @patch.object(Request, 'send_request_to_path', send_request) def it_returns_a_collection_proxy_for_all_without_making_any_requests(self): # noqa + client = Client() # prove a call to send_request_to_path will raise an error with assert_raises(AssertionError): send_request() - all = Admin.all() + all = client.admins.all() self.assertIsInstance(all, CollectionProxy) diff --git a/tests/unit/test_collection_proxy.py b/tests/unit/test_collection_proxy.py index cbc33dfc..e94ab261 100644 --- a/tests/unit/test_collection_proxy.py +++ b/tests/unit/test_collection_proxy.py @@ -2,8 +2,7 @@ import unittest -from intercom import Intercom -from intercom import User +from intercom.client import Client from mock import call from mock import patch from nose.tools import eq_ @@ -13,12 +12,15 @@ class CollectionProxyTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_stops_iterating_if_no_next_link(self): body = page_of_users(include_next_link=False) - with patch.object(Intercom, 'get', return_value=body) as mock_method: - emails = [user.email for user in User.all()] - mock_method.assert_called_once_with('/users') + with patch.object(Client, 'get', return_value=body) as mock_method: + emails = [user.email for user in self.client.users.all()] + mock_method.assert_called_once_with('/users', {}) eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com']) # noqa @istest @@ -26,23 +28,23 @@ def it_keeps_iterating_if_next_link(self): page1 = page_of_users(include_next_link=True) page2 = page_of_users(include_next_link=False) side_effect = [page1, page2] - with patch.object(Intercom, 'get', side_effect=side_effect) as mock_method: # noqa - emails = [user.email for user in User.all()] - eq_([call('/users'), call('https://api.intercom.io/users?per_page=50&page=2')], # noqa + with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa + emails = [user.email for user in self.client.users.all()] + eq_([call('/users', {}), call('https://api.intercom.io/users?per_page=50&page=2', {})], # noqa mock_method.mock_calls) eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com'] * 2) # noqa @istest def it_supports_indexed_array_access(self): body = page_of_users(include_next_link=False) - with patch.object(Intercom, 'get', return_value=body) as mock_method: - eq_(User.all()[0].email, 'user1@example.com') - mock_method.assert_called_once_with('/users') + with patch.object(Client, 'get', return_value=body) as mock_method: + eq_(self.client.users.all()[0].email, 'user1@example.com') + mock_method.assert_called_once_with('/users', {}) @istest def it_supports_querying(self): body = page_of_users(include_next_link=False) - with patch.object(Intercom, 'get', return_value=body) as mock_method: - emails = [user.email for user in User.find_all(tag_name='Taggart J')] # noqa + with patch.object(Client, 'get', return_value=body) as mock_method: + emails = [user.email for user in self.client.users.find_all(tag_name='Taggart J')] # noqa eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com']) # noqa - mock_method.assert_called_once_with('/users', tag_name='Taggart J') + mock_method.assert_called_once_with('/users', {'tag_name': 'Taggart J'}) # noqa diff --git a/tests/unit/test_company.py b/tests/unit/test_company.py index 3a406baf..d6de0c79 100644 --- a/tests/unit/test_company.py +++ b/tests/unit/test_company.py @@ -3,8 +3,8 @@ import intercom import unittest -from intercom import Company -from intercom import Intercom +from intercom.client import Client +from intercom.company import Company from mock import call from mock import patch from nose.tools import assert_raises @@ -14,32 +14,29 @@ class CompanyTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_raises_error_if_no_response_on_find(self): - with patch.object(Intercom, 'get', return_value=None) as mock_method: + with patch.object(Client, 'get', return_value=None) as mock_method: with assert_raises(intercom.HttpError): - Company.find(company_id='4') - mock_method.assert_called_once_with('/companies', company_id='4') + self.client.companies.find(company_id='4') + mock_method.assert_called_once_with('/companies', {'company_id': '4'}) @istest def it_raises_error_if_no_response_on_find_all(self): - with patch.object(Intercom, 'get', return_value=None) as mock_method: + with patch.object(Client, 'get', return_value=None) as mock_method: with assert_raises(intercom.HttpError): - [x for x in Company.all()] - mock_method.assert_called_once_with('/companies') + [x for x in self.client.companies.all()] + mock_method.assert_called_once_with('/companies', {}) @istest def it_raises_error_on_load(self): - data = { - 'type': 'user', - 'id': 'aaaaaaaaaaaaaaaaaaaaaaaa', - 'company_id': '4', - 'name': 'MyCo' - } - side_effect = [data, None] - with patch.object(Intercom, 'get', side_effect=side_effect) as mock_method: # noqa - company = Company.find(company_id='4') + company = Company() + company.id = '4' + side_effect = [None] + with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa with assert_raises(intercom.HttpError): - company.load() - eq_([call('/companies', company_id='4'), call('/companies/aaaaaaaaaaaaaaaaaaaaaaaa')], # noqa - mock_method.mock_calls) + self.client.companies.load(company) + eq_([call('/companies/4', {})], mock_method.mock_calls) diff --git a/tests/unit/test_event.py b/tests/unit/test_event.py index 9068dbc2..2b953413 100644 --- a/tests/unit/test_event.py +++ b/tests/unit/test_event.py @@ -4,9 +4,8 @@ import unittest from datetime import datetime -from intercom import User -from intercom import Intercom -from intercom import Event +from intercom.client import Client +from intercom.user import User from mock import patch from nose.tools import istest @@ -14,6 +13,7 @@ class EventTest(unittest.TestCase): def setUp(self): # noqa + self.client = Client() now = time.mktime(datetime.utcnow().timetuple()) self.user = User( email="jim@example.com", @@ -35,9 +35,9 @@ def it_creates_an_event_with_metadata(self): } } - with patch.object(Intercom, 'post', return_value=data) as mock_method: - Event.create(**data) - mock_method.assert_called_once_with('/events/', **data) + with patch.object(Client, 'post', return_value=data) as mock_method: + self.client.events.create(**data) + mock_method.assert_called_once_with('/events/', data) @istest def it_creates_an_event_without_metadata(self): @@ -45,6 +45,106 @@ def it_creates_an_event_without_metadata(self): 'event_name': 'sale of item', 'email': 'joe@example.com', } - with patch.object(Intercom, 'post', return_value=data) as mock_method: - Event.create(**data) - mock_method.assert_called_once_with('/events/', **data) + with patch.object(Client, 'post', return_value=data) as mock_method: + self.client.events.create(**data) + mock_method.assert_called_once_with('/events/', data) + +class DescribeBulkOperations(unittest.TestCase): # noqa + def setUp(self): # noqa + self.client = Client() + + self.job = { + "app_id": "app_id", + "id": "super_awesome_job", + "created_at": 1446033421, + "completed_at": 1446048736, + "closing_at": 1446034321, + "updated_at": 1446048736, + "name": "api_bulk_job", + "state": "completed", + "links": { + "error": "https://api.intercom.io/jobs/super_awesome_job/error", + "self": "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks": [ + { + "id": "super_awesome_task", + "item_count": 2, + "created_at": 1446033421, + "started_at": 1446033709, + "completed_at": 1446033709, + "state": "completed" + } + ] + } + + self.bulk_request = { + "items": [ + { + "method": "post", + "data_type": "event", + "data": { + "event_name": "ordered-item", + "created_at": 1438944980, + "user_id": "314159", + "metadata": { + "order_date": 1438944980, + "stripe_invoice": "inv_3434343434" + } + } + }, + { + "method": "post", + "data_type": "event", + "data": { + "event_name": "invited-friend", + "created_at": 1438944979, + "user_id": "314159", + "metadata": { + "invitee_email": "pi@example.org", + "invite_code": "ADDAFRIEND" + } + } + } + ] + } + + self.events = [ + { + "event_name": "ordered-item", + "created_at": 1438944980, + "user_id": "314159", + "metadata": { + "order_date": 1438944980, + "stripe_invoice": "inv_3434343434" + } + }, + { + "event_name": "invited-friend", + "created_at": 1438944979, + "user_id": "314159", + "metadata": { + "invitee_email": "pi@example.org", + "invite_code": "ADDAFRIEND" + } + } + ] + + @istest + def it_submits_a_bulk_job(self): # noqa + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.events.submit_bulk_job(create_items=self.events) + mock_method.assert_called_once_with('/bulk/events', self.bulk_request) + + @istest + def it_adds_events_to_an_existing_bulk_job(self): # noqa + self.bulk_request['job'] = {'id': 'super_awesome_job'} + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.events.submit_bulk_job( + create_items=self.events, job_id='super_awesome_job') + mock_method.assert_called_once_with('/bulk/events', self.bulk_request) + + @istest + def it_does_not_submit_delete_jobs(self): # noqa + with self.assertRaises(Exception): + self.client.events.submit_bulk_job(delete_items=self.events) diff --git a/tests/unit/test_intercom.py b/tests/unit/test_intercom.py deleted file mode 100644 index b6534d82..00000000 --- a/tests/unit/test_intercom.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- - -import intercom -import mock -import time -import unittest - -from datetime import datetime -from nose.tools import assert_raises -from nose.tools import eq_ -from nose.tools import istest - - -class ExpectingArgumentsTest(unittest.TestCase): - - def setUp(self): # noqa - self.intercom = intercom.Intercom - self.intercom.app_id = 'abc123' - self.intercom.app_api_key = 'super-secret-key' - - @istest - def it_raises_argumenterror_if_no_app_id_or_app_api_key_specified(self): # noqa - self.intercom.app_id = None - self.intercom.app_api_key = None - with assert_raises(intercom.ArgumentError): - self.intercom.target_base_url - - @istest - def it_returns_the_app_id_and_app_api_key_previously_set(self): - eq_(self.intercom.app_id, 'abc123') - eq_(self.intercom.app_api_key, 'super-secret-key') - - @istest - def it_defaults_to_https_to_api_intercom_io(self): - eq_(self.intercom.target_base_url, - 'https://abc123:super-secret-key@api.intercom.io') - - -class OverridingProtocolHostnameTest(unittest.TestCase): - def setUp(self): # noqa - self.intercom = intercom.Intercom - self.protocol = self.intercom.protocol - self.hostname = self.intercom.hostname - self.intercom.endpoints = None - - def tearDown(self): # noqa - self.intercom.protocol = self.protocol - self.intercom.hostname = self.hostname - self.intercom.endpoints = ["https://api.intercom.io"] - - @istest - def it_allows_overriding_of_the_endpoint_and_protocol(self): - self.intercom.protocol = "http" - self.intercom.hostname = "localhost:3000" - eq_( - self.intercom.target_base_url, - "http://abc123:super-secret-key@localhost:3000") - - @istest - def it_prefers_endpoints(self): - self.intercom.endpoint = "https://localhost:7654" - eq_(self.intercom.target_base_url, - "https://abc123:super-secret-key@localhost:7654") - - # turn off the shuffle - with mock.patch("random.shuffle") as mock_shuffle: - mock_shuffle.return_value = ["http://example.com", "https://localhost:7654"] # noqa - self.intercom.endpoints = ["http://example.com", "https://localhost:7654"] # noqa - eq_(self.intercom.target_base_url, - 'http://abc123:super-secret-key@example.com') - - @istest - def it_has_endpoints(self): - eq_(self.intercom.endpoints, ["https://api.intercom.io"]) - self.intercom.endpoints = ["http://example.com", "https://localhost:7654"] # noqa - eq_(self.intercom.endpoints, ["http://example.com", "https://localhost:7654"]) # noqa - - @istest - def it_should_randomize_endpoints_if_last_checked_endpoint_is_gt_5_minutes_ago(self): # noqa - now = time.mktime(datetime.utcnow().timetuple()) - self.intercom._endpoint_randomized_at = now - self.intercom.endpoints = ["http://alternative"] - self.intercom.current_endpoint = "http://start" - - self.intercom._endpoint_randomized_at = now - 120 - eq_(self.intercom.current_endpoint, "http://start") - self.intercom._endpoint_randomized_at = now - 360 - eq_(self.intercom.current_endpoint, "http://alternative") diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py new file mode 100644 index 00000000..337b1471 --- /dev/null +++ b/tests/unit/test_job.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- # noqa + +import unittest + +from intercom.client import Client +from mock import patch +from nose.tools import istest + + +class DescribeJobs(unittest.TestCase): # noqa + def setUp(self): # noqa + self.client = Client() + + self.job = { + "app_id": "app_id", + "id": "super_awesome_job", + "created_at": 1446033421, + "completed_at": 1446048736, + "closing_at": 1446034321, + "updated_at": 1446048736, + "name": "api_bulk_job", + "state": "completed", + "links": { + "error": "https://api.intercom.io/jobs/super_awesome_job/error", + "self": "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks": [ + { + "id": "super_awesome_task", + "item_count": 2, + "created_at": 1446033421, + "started_at": 1446033709, + "completed_at": 1446033709, + "state": "completed" + } + ] + } + + self.error_feed = { + "app_id": "app_id", + "job_id": "super_awesome_job", + "pages": {}, + "items": [] + } + + @istest + def it_gets_a_job(self): # noqa + with patch.object(Client, 'get', return_value=self.job) as mock_method: # noqa + self.client.jobs.find(id='super_awesome_job') + mock_method.assert_called_once_with('/jobs/super_awesome_job', {}) + + @istest + def it_gets_a_jobs_error_feed(self): # noqa + with patch.object(Client, 'get', return_value=self.error_feed) as mock_method: # noqa + self.client.jobs.errors(id='super_awesome_job') + mock_method.assert_called_once_with('/jobs/super_awesome_job/error', {}) diff --git a/tests/unit/test_lead.py b/tests/unit/test_lead.py new file mode 100644 index 00000000..cca2debd --- /dev/null +++ b/tests/unit/test_lead.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- # noqa + +import mock +import unittest + +from intercom.collection_proxy import CollectionProxy +from intercom.client import Client +from intercom.lead import Lead +from intercom.user import User +from mock import patch +from nose.tools import istest +from tests.unit import get_user + + +class LeadTest(unittest.TestCase): # noqa + + def setUp(self): # noqa + self.client = Client() + + @istest + def it_should_be_listable(self): # noqa + proxy = self.client.leads.all() + self.assertEquals('contacts', proxy.resource_name) + self.assertEquals('/contacts', proxy.finder_url) + self.assertEquals(Lead, proxy.resource_class) + + @istest + def it_should_not_throw_errors_when_there_are_no_parameters(self): # noqa + with patch.object(Client, 'post') as mock_method: # noqa + self.client.leads.create() + + @istest + def it_can_update_a_lead_with_an_id(self): # noqa + lead = Lead(id="de45ae78gae1289cb") + with patch.object(Client, 'put') as mock_method: # noqa + self.client.leads.save(lead) + mock_method.assert_called_once_with( + '/contacts/de45ae78gae1289cb', {'custom_attributes': {}}) + + @istest + def it_can_convert(self): # noqa + lead = Lead.from_api({'user_id': 'contact_id'}) + user = User.from_api({'id': 'user_id'}) + + with patch.object(Client, 'post', returns=get_user()) as mock_method: # noqa + self.client.leads.convert(lead, user) + mock_method.assert_called_once_with( + '/contacts/convert', + { + 'contact': {'user_id': lead.user_id}, + 'user': {'id': user.id} + }) + + @istest + def it_returns_a_collectionproxy_for_all_without_making_any_requests(self): # noqa + with mock.patch('intercom.request.Request.send_request_to_path', new_callable=mock.NonCallableMock): # noqa + res = self.client.leads.all() + self.assertIsInstance(res, CollectionProxy) + + @istest + def it_deletes_a_contact(self): # noqa + lead = Lead(id="1") + with patch.object(Client, 'delete') as mock_method: # noqa + self.client.leads.delete(lead) + mock_method.assert_called_once_with('/contacts/1', {}) diff --git a/tests/unit/test_message.py b/tests/unit/test_message.py index 592292ac..be033c84 100644 --- a/tests/unit/test_message.py +++ b/tests/unit/test_message.py @@ -4,9 +4,8 @@ import unittest from datetime import datetime -from intercom import Intercom -from intercom import User -from intercom import Message +from intercom.client import Client +from intercom.user import User from mock import patch from nose.tools import eq_ from nose.tools import istest @@ -15,6 +14,7 @@ class MessageTest(unittest.TestCase): def setUp(self): # noqa + self.client = Client() now = time.mktime(datetime.utcnow().timetuple()) self.user = User( email="jim@example.com", @@ -32,9 +32,9 @@ def it_creates_a_user_message_with_string_keys(self): }, 'body': 'halp' } - with patch.object(Intercom, 'post', return_value=data) as mock_method: - message = Message.create(**data) - mock_method.assert_called_once_with('/messages/', **data) + with patch.object(Client, 'post', return_value=data) as mock_method: + message = self.client.messages.create(**data) + mock_method.assert_called_once_with('/messages/', data) eq_('halp', message.body) @istest @@ -52,8 +52,8 @@ def it_creates_an_admin_message(self): 'message_type': 'inapp' } - with patch.object(Intercom, 'post', return_value=data) as mock_method: - message = Message.create(**data) - mock_method.assert_called_once_with('/messages/', **data) + with patch.object(Client, 'post', return_value=data) as mock_method: + message = self.client.messages.create(**data) + mock_method.assert_called_once_with('/messages/', data) eq_('halp', message.body) eq_('inapp', message.message_type) diff --git a/tests/unit/test_note.py b/tests/unit/test_note.py index 7a9478e7..ddd31888 100644 --- a/tests/unit/test_note.py +++ b/tests/unit/test_note.py @@ -2,8 +2,8 @@ import unittest -from intercom import Intercom -from intercom import Note +from intercom.client import Client +from intercom.note import Note from mock import patch from nose.tools import eq_ from nose.tools import istest @@ -11,15 +11,18 @@ class NoteTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_creates_a_note(self): data = { 'body': 'Note to leave on user
', 'created_at': 1234567890 } - with patch.object(Intercom, 'post', return_value=data) as mock_method: - note = Note.create(body="Note to leave on user") - mock_method.assert_called_once_with('/notes/', body="Note to leave on user") # noqa + with patch.object(Client, 'post', return_value=data) as mock_method: + note = self.client.notes.create(body="Note to leave on user") + mock_method.assert_called_once_with('/notes/', {'body': "Note to leave on user"}) # noqa eq_(note.body, "Note to leave on user
") @istest @@ -33,7 +36,7 @@ def it_sets_gets_allowed_keys(self): params_keys.sort() note = Note(**params) - note_dict = note.to_dict + note_dict = note.to_dict() note_keys = list(note_dict.keys()) note_keys.sort() diff --git a/tests/unit/test_notification.py b/tests/unit/test_notification.py index 4c4c0f65..0ab3cb83 100644 --- a/tests/unit/test_notification.py +++ b/tests/unit/test_notification.py @@ -2,7 +2,7 @@ import unittest -from intercom import Notification +from intercom.notification import Notification from intercom.utils import create_class_instance from nose.tools import eq_ from nose.tools import istest diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index 79905cb1..5b14d2e6 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -4,10 +4,9 @@ import json import unittest -from intercom import Intercom -from intercom import Request +from intercom.client import Client +from intercom.request import Request from intercom import UnexpectedError -from mock import Mock from mock import patch from nose.tools import assert_raises from nose.tools import eq_ @@ -18,53 +17,62 @@ class RequestTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_raises_resource_not_found(self): - resp = mock_response('{}', status_code=404) + resp = mock_response(None, status_code=404) with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.ResourceNotFound): - Request.send_request_to_path('GET', 'notes', ('x', 'y'), resp) + request = Request('GET', 'notes') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_raises_authentication_error_unauthorized(self): - resp = mock_response('{}', status_code=401) + resp = mock_response(None, status_code=401) with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.AuthenticationError): - Request.send_request_to_path('GET', 'notes', ('x', 'y'), resp) + request = Request('GET', 'notes') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_raises_authentication_error_forbidden(self): - resp = mock_response('{}', status_code=403) + resp = mock_response(None, status_code=403) with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.AuthenticationError): - Request.send_request_to_path('GET', 'notes', ('x', 'y'), resp) + request = Request('GET', 'notes') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_raises_server_error(self): - resp = Mock(encoding="utf-8", content='{}', status_code=500) + resp = mock_response(None, status_code=500) with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.ServerError): - Request.send_request_to_path('GET', 'notes', ('x', 'y'), resp) + request = Request('GET', 'notes') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_raises_bad_gateway_error(self): - resp = mock_response('{}', status_code=502) + resp = mock_response(None, status_code=502) with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.BadGatewayError): - Request.send_request_to_path('GET', 'notes', ('x', 'y'), resp) + request = Request('GET', 'notes') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_raises_service_unavailable_error(self): - resp = mock_response('{}', status_code=503) + resp = mock_response(None, status_code=503) with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.ServiceUnavailableError): - Request.send_request_to_path('GET', 'notes', ('x', 'y'), resp) + request = Request('GET', 'notes') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_raises_an_unexpected_typed_error(self): @@ -82,7 +90,7 @@ def it_raises_an_unexpected_typed_error(self): with patch('requests.request') as mock_method: mock_method.return_value = resp try: - Intercom.get('/users') + self.client.get('/users', {}) self.fail('UnexpectedError not raised.') except (UnexpectedError) as err: ok_("The error of type 'hopper' is not recognized" in err.message) # noqa @@ -104,7 +112,7 @@ def it_raises_an_unexpected_untyped_error(self): with patch('requests.request') as mock_method: mock_method.return_value = resp try: - Intercom.get('/users') + self.client.get('/users', {}) self.fail('UnexpectedError not raised.') except (UnexpectedError) as err: ok_("An unexpected error occured." in err.message) @@ -130,7 +138,7 @@ def it_raises_a_bad_request_error(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.BadRequestError): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_raises_an_authentication_error(self): @@ -151,7 +159,7 @@ def it_raises_an_authentication_error(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.AuthenticationError): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_raises_resource_not_found_by_type(self): @@ -169,7 +177,7 @@ def it_raises_resource_not_found_by_type(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.ResourceNotFound): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_raises_rate_limit_exceeded(self): @@ -187,7 +195,7 @@ def it_raises_rate_limit_exceeded(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.RateLimitExceeded): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_raises_a_service_unavailable_error(self): @@ -205,7 +213,7 @@ def it_raises_a_service_unavailable_error(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.ServiceUnavailableError): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_raises_a_multiple_matching_users_error(self): @@ -223,7 +231,7 @@ def it_raises_a_multiple_matching_users_error(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.MultipleMatchingUsersError): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_handles_no_error_type(self): @@ -242,7 +250,7 @@ def it_handles_no_error_type(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.MultipleMatchingUsersError): - Intercom.get('/users') + self.client.get('/users', {}) payload = { 'errors': [ @@ -259,19 +267,21 @@ def it_handles_no_error_type(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(intercom.BadRequestError): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_handles_empty_responses(self): resp = mock_response('', status_code=202) with patch('requests.request') as mock_method: mock_method.return_value = resp - Request.send_request_to_path('GET', 'events', ('x', 'y'), resp) + request = Request('GET', 'events') + request.send_request_to_path('', ('x', 'y'), resp) resp = mock_response(' ', status_code=202) with patch('requests.request') as mock_method: mock_method.return_value = resp - Request.send_request_to_path('GET', 'events', ('x', 'y'), resp) + request = Request('GET', 'events') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_handles_no_encoding(self): @@ -281,7 +291,8 @@ def it_handles_no_encoding(self): with patch('requests.request') as mock_method: mock_method.return_value = resp - Request.send_request_to_path('GET', 'events', ('x', 'y'), resp) + request = Request('GET', 'events') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_needs_encoding_or_apparent_encoding(self): @@ -297,10 +308,12 @@ def it_needs_encoding_or_apparent_encoding(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(TypeError): - Request.send_request_to_path('GET', 'events', ('x', 'y'), resp) + request = Request('GET', 'events') + request.send_request_to_path('', ('x', 'y'), resp) @istest def it_allows_the_timeout_to_be_changed(self): - eq_(10, intercom.Request.timeout) - intercom.Request.timeout = 3 - eq_(3, intercom.Request.timeout) + from intercom.request import Request + eq_(10, Request.timeout) + Request.timeout = 3 + eq_(3, Request.timeout) diff --git a/tests/unit/test_subscription.py b/tests/unit/test_subscription.py index b7a01ea9..0922ae0c 100644 --- a/tests/unit/test_subscription.py +++ b/tests/unit/test_subscription.py @@ -2,8 +2,7 @@ import unittest -from intercom import Intercom -from intercom import Subscription +from intercom.client import Client from mock import patch from nose.tools import eq_ from nose.tools import istest @@ -12,24 +11,27 @@ class SubscriptionTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_gets_a_subscription(self): - with patch.object(Intercom, 'get', return_value=test_subscription) as mock_method: # noqa - subscription = Subscription.find(id="nsub_123456789") + with patch.object(Client, 'get', return_value=test_subscription) as mock_method: # noqa + subscription = self.client.subscriptions.find(id="nsub_123456789") eq_(subscription.topics[0], "user.created") eq_(subscription.topics[1], "conversation.user.replied") eq_(subscription.self, "https://api.intercom.io/subscriptions/nsub_123456789") - mock_method.assert_called_once_with('/subscriptions/nsub_123456789') # noqa + mock_method.assert_called_once_with('/subscriptions/nsub_123456789', {}) # noqa @istest def it_creates_a_subscription(self): - with patch.object(Intercom, 'post', return_value=test_subscription) as mock_method: # noqa - subscription = Subscription.create( + with patch.object(Client, 'post', return_value=test_subscription) as mock_method: # noqa + subscription = self.client.subscriptions.create( url="http://example.com", topics=["user.created"] ) eq_(subscription.topics[0], "user.created") eq_(subscription.url, "http://example.com") mock_method.assert_called_once_with( - '/subscriptions/', url="http://example.com", topics=["user.created"]) # noqa + '/subscriptions/', {'url': "http://example.com", 'topics': ["user.created"]}) # noqa diff --git a/tests/unit/test_tag.py b/tests/unit/test_tag.py index a9d36dc9..38dfc5d3 100644 --- a/tests/unit/test_tag.py +++ b/tests/unit/test_tag.py @@ -2,8 +2,7 @@ import unittest -from intercom import Intercom -from intercom import Tag +from intercom.client import Client from mock import patch from nose.tools import eq_ from nose.tools import istest @@ -12,19 +11,22 @@ class TagTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_gets_a_tag(self): - with patch.object(Intercom, 'get', return_value=test_tag) as mock_method: # noqa - tag = Tag.find(name="Test Tag") + with patch.object(Client, 'get', return_value=test_tag) as mock_method: # noqa + tag = self.client.tags.find(name="Test Tag") eq_(tag.name, "Test Tag") - mock_method.assert_called_once_with('/tags', name="Test Tag") + mock_method.assert_called_once_with('/tags', {'name': "Test Tag"}) @istest def it_creates_a_tag(self): - with patch.object(Intercom, 'post', return_value=test_tag) as mock_method: # noqa - tag = Tag.create(name="Test Tag") + with patch.object(Client, 'post', return_value=test_tag) as mock_method: # noqa + tag = self.client.tags.create(name="Test Tag") eq_(tag.name, "Test Tag") - mock_method.assert_called_once_with('/tags/', name="Test Tag") + mock_method.assert_called_once_with('/tags/', {'name': "Test Tag"}) @istest def it_tags_users(self): @@ -33,8 +35,8 @@ def it_tags_users(self): 'user_ids': ['abc123', 'def456'], 'tag_or_untag': 'tag' } - with patch.object(Intercom, 'post', return_value=test_tag) as mock_method: # noqa - tag = Tag.create(**params) + with patch.object(Client, 'post', return_value=test_tag) as mock_method: # noqa + tag = self.client.tags.create(**params) eq_(tag.name, "Test Tag") eq_(tag.tagged_user_count, 2) - mock_method.assert_called_once_with('/tags/', **params) + mock_method.assert_called_once_with('/tags/', params) diff --git a/tests/unit/test_user.py b/tests/unit/test_user.py index 59add0cf..63132033 100644 --- a/tests/unit/test_user.py +++ b/tests/unit/test_user.py @@ -8,8 +8,8 @@ from datetime import datetime from intercom.collection_proxy import CollectionProxy from intercom.lib.flat_store import FlatStore -from intercom import Intercom -from intercom import User +from intercom.client import Client +from intercom.user import User from intercom import MultipleMatchingUsersError from intercom.utils import create_class_instance from mock import patch @@ -23,13 +23,16 @@ class UserTest(unittest.TestCase): + def setUp(self): + self.client = Client() + @istest def it_to_dict_itself(self): created_at = datetime.utcnow() user = User( email="jim@example.com", user_id="12345", created_at=created_at, name="Jim Bob") - as_dict = user.to_dict + as_dict = user.to_dict() eq_(as_dict["email"], "jim@example.com") eq_(as_dict["user_id"], "12345") eq_(as_dict["created_at"], time.mktime(created_at.timetuple())) @@ -121,10 +124,12 @@ def it_allows_update_last_request_at(self): 'update_last_request_at': True, 'custom_attributes': {} } - with patch.object(Intercom, 'post', return_value=payload) as mock_method: - User.create(user_id='1224242', update_last_request_at=True) + with patch.object(Client, 'post', return_value=payload) as mock_method: + self.client.users.create( + user_id='1224242', update_last_request_at=True) mock_method.assert_called_once_with( - '/users/', update_last_request_at=True, user_id='1224242') + '/users/', + {'update_last_request_at': True, 'user_id': '1224242'}) @istest def it_allows_easy_setting_of_custom_data(self): @@ -136,7 +141,7 @@ def it_allows_easy_setting_of_custom_data(self): user.custom_attributes["other"] = now_ts user.custom_attributes["thing"] = "yay" attrs = {"mad": 123, "other": now_ts, "thing": "yay"} - eq_(user.to_dict["custom_attributes"], attrs) + eq_(user.to_dict()["custom_attributes"], attrs) @istest def it_allows_easy_setting_of_multiple_companies(self): @@ -146,7 +151,7 @@ def it_allows_easy_setting_of_multiple_companies(self): {"name": "Test", "company_id": "9"}, ] user.companies = companies - eq_(user.to_dict["companies"], companies) + eq_(user.to_dict()["companies"], companies) @istest def it_rejects_nested_data_structures_in_custom_attributes(self): @@ -166,16 +171,15 @@ def it_rejects_nested_data_structures_in_custom_attributes(self): @istest def it_fetches_a_user(self): - with patch.object(Intercom, 'get', return_value=get_user()) as mock_method: # noqa - user = User.find(email='somebody@example.com') + with patch.object(Client, 'get', return_value=get_user()) as mock_method: # noqa + user = self.client.users.find(email='somebody@example.com') eq_(user.email, 'bob@example.com') eq_(user.name, 'Joe Schmoe') - mock_method.assert_called_once_with('/users', email='somebody@example.com') # noqa + mock_method.assert_called_once_with( + '/users', {'email': 'somebody@example.com'}) # noqa @istest - # @httpretty.activate def it_saves_a_user_always_sends_custom_attributes(self): - user = User(email="jo@example.com", user_id="i-1224242") body = { 'email': 'jo@example.com', @@ -183,21 +187,18 @@ def it_saves_a_user_always_sends_custom_attributes(self): 'custom_attributes': {} } - with patch.object(Intercom, 'post', return_value=body) as mock_method: - user.save() + with patch.object(Client, 'post', return_value=body) as mock_method: + user = User(email="jo@example.com", user_id="i-1224242") + self.client.users.save(user) eq_(user.email, 'jo@example.com') eq_(user.custom_attributes, {}) mock_method.assert_called_once_with( '/users', - email="jo@example.com", user_id="i-1224242", - custom_attributes={}) + {'email': "jo@example.com", 'user_id': "i-1224242", + 'custom_attributes': {}}) @istest def it_saves_a_user_with_a_company(self): - user = User( - email="jo@example.com", user_id="i-1224242", - company={'company_id': 6, 'name': 'Intercom'}) - body = { 'email': 'jo@example.com', 'user_id': 'i-1224242', @@ -206,21 +207,21 @@ def it_saves_a_user_with_a_company(self): 'name': 'Intercom' }] } - with patch.object(Intercom, 'post', return_value=body) as mock_method: - user.save() + with patch.object(Client, 'post', return_value=body) as mock_method: + user = User( + email="jo@example.com", user_id="i-1224242", + company={'company_id': 6, 'name': 'Intercom'}) + self.client.users.save(user) eq_(user.email, 'jo@example.com') eq_(len(user.companies), 1) mock_method.assert_called_once_with( '/users', - email="jo@example.com", user_id="i-1224242", - company={'company_id': 6, 'name': 'Intercom'}, - custom_attributes={}) + {'email': "jo@example.com", 'user_id': "i-1224242", + 'company': {'company_id': 6, 'name': 'Intercom'}, + 'custom_attributes': {}}) @istest def it_saves_a_user_with_companies(self): - user = User( - email="jo@example.com", user_id="i-1224242", - companies=[{'company_id': 6, 'name': 'Intercom'}]) body = { 'email': 'jo@example.com', 'user_id': 'i-1224242', @@ -229,15 +230,18 @@ def it_saves_a_user_with_companies(self): 'name': 'Intercom' }] } - with patch.object(Intercom, 'post', return_value=body) as mock_method: - user.save() + with patch.object(Client, 'post', return_value=body) as mock_method: + user = User( + email="jo@example.com", user_id="i-1224242", + companies=[{'company_id': 6, 'name': 'Intercom'}]) + self.client.users.save(user) eq_(user.email, 'jo@example.com') eq_(len(user.companies), 1) mock_method.assert_called_once_with( '/users', - email="jo@example.com", user_id="i-1224242", - companies=[{'company_id': 6, 'name': 'Intercom'}], - custom_attributes={}) + {'email': "jo@example.com", 'user_id': "i-1224242", + 'companies': [{'company_id': 6, 'name': 'Intercom'}], + 'custom_attributes': {}}) @istest def it_can_save_a_user_with_a_none_email(self): @@ -253,23 +257,23 @@ def it_can_save_a_user_with_a_none_email(self): 'name': 'Intercom' }] } - with patch.object(Intercom, 'post', return_value=body) as mock_method: - user.save() + with patch.object(Client, 'post', return_value=body) as mock_method: + self.client.users.save(user) ok_(user.email is None) eq_(user.user_id, 'i-1224242') mock_method.assert_called_once_with( '/users', - email=None, user_id="i-1224242", - companies=[{'company_id': 6, 'name': 'Intercom'}], - custom_attributes={}) + {'email': None, 'user_id': "i-1224242", + 'companies': [{'company_id': 6, 'name': 'Intercom'}], + 'custom_attributes': {}}) @istest def it_deletes_a_user(self): user = User(id="1") - with patch.object(Intercom, 'delete', return_value={}) as mock_method: - user = user.delete() + with patch.object(Client, 'delete', return_value={}) as mock_method: + user = self.client.users.delete(user) eq_(user.id, "1") - mock_method.assert_called_once_with('/users/1/') + mock_method.assert_called_once_with('/users/1', {}) @istest def it_can_use_user_create_for_convenience(self): @@ -278,10 +282,11 @@ def it_can_use_user_create_for_convenience(self): 'user_id': 'i-1224242', 'custom_attributes': {} } - with patch.object(Intercom, 'post', return_value=payload) as mock_method: # noqa - user = User.create(email="jo@example.com", user_id="i-1224242") - eq_(payload, user.to_dict) - mock_method.assert_called_once_with('/users/', email="jo@example.com", user_id="i-1224242") # noqa + with patch.object(Client, 'post', return_value=payload) as mock_method: # noqa + user = self.client.users.create(email="jo@example.com", user_id="i-1224242") # noqa + eq_(payload, user.to_dict()) + mock_method.assert_called_once_with( + '/users/', {'email': "jo@example.com", 'user_id': "i-1224242"}) # noqa @istest def it_updates_the_user_with_attributes_set_by_the_server(self): @@ -291,10 +296,12 @@ def it_updates_the_user_with_attributes_set_by_the_server(self): 'custom_attributes': {}, 'session_count': 4 } - with patch.object(Intercom, 'post', return_value=payload) as mock_method: - user = User.create(email="jo@example.com", user_id="i-1224242") - eq_(payload, user.to_dict) - mock_method.assert_called_once_with('/users/', email="jo@example.com", user_id="i-1224242") # noqa + with patch.object(Client, 'post', return_value=payload) as mock_method: # noqa + user = self.client.users.create(email="jo@example.com", user_id="i-1224242") # noqa + eq_(payload, user.to_dict()) + mock_method.assert_called_once_with( + '/users/', + {'email': "jo@example.com", 'user_id': "i-1224242"}) # noqa @istest def it_allows_setting_dates_to_none_without_converting_them_to_0(self): @@ -303,10 +310,10 @@ def it_allows_setting_dates_to_none_without_converting_them_to_0(self): 'custom_attributes': {}, 'remote_created_at': None } - with patch.object(Intercom, 'post', return_value=payload) as mock_method: - user = User.create(email="jo@example.com", remote_created_at=None) + with patch.object(Client, 'post', return_value=payload) as mock_method: + user = self.client.users.create(email="jo@example.com", remote_created_at=None) # noqa ok_(user.remote_created_at is None) - mock_method.assert_called_once_with('/users/', email="jo@example.com", remote_created_at=None) # noqa + mock_method.assert_called_once_with('/users/', {'email': "jo@example.com", 'remote_created_at': None}) # noqa @istest def it_gets_sets_rw_keys(self): @@ -322,9 +329,9 @@ def it_gets_sets_rw_keys(self): user = User(**payload) expected_keys = ['custom_attributes'] expected_keys.extend(list(payload.keys())) - eq_(sorted(expected_keys), sorted(user.to_dict.keys())) + eq_(sorted(expected_keys), sorted(user.to_dict().keys())) for key in list(payload.keys()): - eq_(payload[key], user.to_dict[key]) + eq_(payload[key], user.to_dict()[key]) @istest def it_will_allow_extra_attributes_in_response_from_api(self): @@ -333,16 +340,10 @@ def it_will_allow_extra_attributes_in_response_from_api(self): @istest def it_returns_a_collectionproxy_for_all_without_making_any_requests(self): - with mock.patch('intercom.Request.send_request_to_path', new_callable=mock.NonCallableMock): # noqa - res = User.all() + with mock.patch('intercom.request.Request.send_request_to_path', new_callable=mock.NonCallableMock): # noqa + res = self.client.users.all() self.assertIsInstance(res, CollectionProxy) - @istest - def it_returns_the_total_number_of_users(self): - with mock.patch.object(User, 'count') as mock_count: - mock_count.return_value = 100 - eq_(100, User.count()) - @istest def it_raises_a_multiple_matching_users_error_when_receiving_a_conflict(self): # noqa payload = { @@ -361,7 +362,7 @@ def it_raises_a_multiple_matching_users_error_when_receiving_a_conflict(self): with patch('requests.request') as mock_method: mock_method.return_value = resp with assert_raises(MultipleMatchingUsersError): - Intercom.get('/users') + self.client.get('/users', {}) @istest def it_handles_accented_characters(self): @@ -373,7 +374,7 @@ def it_handles_accented_characters(self): resp = mock_response(content) with patch('requests.request') as mock_method: mock_method.return_value = resp - user = User.find(email='bob@example.com') + user = self.client.users.find(email='bob@example.com') try: # Python 2 eq_(unicode('Jóe Schmö', 'utf-8'), user.name) @@ -385,6 +386,8 @@ def it_handles_accented_characters(self): class DescribeIncrementingCustomAttributeFields(unittest.TestCase): def setUp(self): # noqa + self.client = Client() + created_at = datetime.utcnow() params = { 'email': 'jo@example.com', @@ -401,28 +404,28 @@ def setUp(self): # noqa @istest def it_increments_up_by_1_with_no_args(self): self.user.increment('mad') - eq_(self.user.to_dict['custom_attributes']['mad'], 124) + eq_(self.user.to_dict()['custom_attributes']['mad'], 124) @istest def it_increments_up_by_given_value(self): self.user.increment('mad', 4) - eq_(self.user.to_dict['custom_attributes']['mad'], 127) + eq_(self.user.to_dict()['custom_attributes']['mad'], 127) @istest def it_increments_down_by_given_value(self): self.user.increment('mad', -1) - eq_(self.user.to_dict['custom_attributes']['mad'], 122) + eq_(self.user.to_dict()['custom_attributes']['mad'], 122) @istest def it_can_increment_new_custom_data_fields(self): self.user.increment('new_field', 3) - eq_(self.user.to_dict['custom_attributes']['new_field'], 3) + eq_(self.user.to_dict()['custom_attributes']['new_field'], 3) @istest def it_can_call_increment_on_the_same_key_twice_and_increment_by_2(self): # noqa self.user.increment('mad') self.user.increment('mad') - eq_(self.user.to_dict['custom_attributes']['mad'], 125) + eq_(self.user.to_dict()['custom_attributes']['mad'], 125) @istest def it_can_save_after_increment(self): # noqa @@ -438,9 +441,102 @@ def it_can_save_after_increment(self): # noqa 'name': 'Intercom' }] } - with patch.object(Intercom, 'post', return_value=body) as mock_method: # noqa + with patch.object(Client, 'post', return_value=body) as mock_method: # noqa user.increment('mad') - eq_(user.to_dict['custom_attributes']['mad'], 1) - user.save() - ok_('email' not in user.identity_hash) - ok_('user_id' in user.identity_hash) + eq_(user.to_dict()['custom_attributes']['mad'], 1) + self.client.users.save(user) + + +class DescribeBulkOperations(unittest.TestCase): # noqa + + def setUp(self): # noqa + self.client = Client() + + self.job = { + "app_id": "app_id", + "id": "super_awesome_job", + "created_at": 1446033421, + "completed_at": 1446048736, + "closing_at": 1446034321, + "updated_at": 1446048736, + "name": "api_bulk_job", + "state": "completed", + "links": { + "error": "https://api.intercom.io/jobs/super_awesome_job/error", + "self": "https://api.intercom.io/jobs/super_awesome_job" + }, + "tasks": [ + { + "id": "super_awesome_task", + "item_count": 2, + "created_at": 1446033421, + "started_at": 1446033709, + "completed_at": 1446033709, + "state": "completed" + } + ] + } + + self.bulk_request = { + "items": [ + { + "method": "post", + "data_type": "user", + "data": { + "user_id": 25, + "email": "alice@example.com" + } + }, + { + "method": "delete", + "data_type": "user", + "data": { + "user_id": 26, + "email": "bob@example.com" + } + } + ] + } + + self.users_to_create = [ + { + "user_id": 25, + "email": "alice@example.com" + } + ] + + self.users_to_delete = [ + { + "user_id": 26, + "email": "bob@example.com" + } + ] + + created_at = datetime.utcnow() + params = { + 'email': 'jo@example.com', + 'user_id': 'i-1224242', + 'custom_attributes': { + 'mad': 123, + 'another': 432, + 'other': time.mktime(created_at.timetuple()), + 'thing': 'yay' + } + } + self.user = User(**params) + + @istest + def it_submits_a_bulk_job(self): # noqa + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.users.submit_bulk_job( + create_items=self.users_to_create, delete_items=self.users_to_delete) + mock_method.assert_called_once_with('/bulk/users', self.bulk_request) + + @istest + def it_adds_users_to_an_existing_bulk_job(self): # noqa + self.bulk_request['job'] = {'id': 'super_awesome_job'} + with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa + self.client.users.submit_bulk_job( + create_items=self.users_to_create, delete_items=self.users_to_delete, + job_id='super_awesome_job') + mock_method.assert_called_once_with('/bulk/users', self.bulk_request) diff --git a/tests/unit/traits/test_api_resource.py b/tests/unit/traits/test_api_resource.py index 92a73ee5..464a32ec 100644 --- a/tests/unit/traits/test_api_resource.py +++ b/tests/unit/traits/test_api_resource.py @@ -57,10 +57,10 @@ def it_exposes_dates_correctly_for_dynamically_defined_getters(self): self.api_resource.foo_at = 1401200468 eq_(datetime.fromtimestamp(1401200468), self.api_resource.foo_at) - # @istest - # def it_throws_regular_error_when_non_existant_getter_is_called_that_is_backed_by_an_instance_variable(self): # noqa - # super(Resource, self.api_resource).__setattr__('bar', 'you cant see me') # noqa - # print (self.api_resource.bar) + @istest + def it_throws_regular_error_when_non_existant_getter_is_called_that_is_backed_by_an_instance_variable(self): # noqa + super(Resource, self.api_resource).__setattr__('bar', 'you cant see me') # noqa + self.api_resource.bar @istest def it_throws_attribute_error_when_non_existent_attribute_is_called(self):