├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── CHANGES.rst ├── INSTALL.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── dev-requirements.txt ├── docs ├── Makefile ├── api │ ├── intercom.api_operations.rst │ ├── intercom.extended_api_operations.rst │ ├── intercom.generic_handlers.rst │ ├── intercom.lib.rst │ ├── intercom.rst │ ├── intercom.traits.rst │ └── modules.rst ├── changelog.rst ├── conf.py ├── development.rst ├── faq.rst ├── index.rst ├── installation.rst └── make.bat ├── intercom ├── __init__.py ├── admin.py ├── api_operations │ ├── __init__.py │ ├── all.py │ ├── bulk.py │ ├── convert.py │ ├── delete.py │ ├── find.py │ ├── find_all.py │ ├── load.py │ ├── save.py │ └── scroll.py ├── client.py ├── collection_proxy.py ├── company.py ├── conversation.py ├── count.py ├── errors.py ├── event.py ├── extended_api_operations │ ├── __init__.py │ ├── tags.py │ └── users.py ├── job.py ├── lead.py ├── lib │ ├── __init__.py │ ├── flat_store.py │ ├── setter_property.py │ └── typed_json_deserializer.py ├── message.py ├── note.py ├── notification.py ├── request.py ├── scroll_collection_proxy.py ├── segment.py ├── service │ ├── __init__.py │ ├── admin.py │ ├── base_service.py │ ├── company.py │ ├── conversation.py │ ├── count.py │ ├── event.py │ ├── job.py │ ├── lead.py │ ├── message.py │ ├── note.py │ ├── segment.py │ ├── subscription.py │ ├── tag.py │ └── user.py ├── subscription.py ├── tag.py ├── traits │ ├── __init__.py │ ├── api_resource.py │ └── incrementable_attributes.py ├── user.py └── utils.py ├── pylint.conf ├── requirements.txt ├── rtd-requirements.txt ├── setup.py └── tests ├── __init__.py ├── integration ├── __init__.py ├── issues │ ├── __init__.py │ ├── test_72.py │ └── test_73.py ├── test_admin.py ├── test_company.py ├── test_conversations.py ├── test_count.py ├── test_notes.py ├── test_segments.py ├── test_tags.py └── test_user.py ├── run_tests.sh └── unit ├── __init__.py ├── lib ├── __init__.py └── test_flat_store.py ├── test_admin.py ├── test_collection_proxy.py ├── test_company.py ├── test_event.py ├── test_import.py ├── test_job.py ├── test_lead.py ├── test_message.py ├── test_note.py ├── test_notification.py ├── test_request.py ├── test_scroll_collection_proxy.py ├── test_subscription.py ├── test_tag.py ├── test_user.py ├── test_utils.py └── traits ├── __init__.py └── test_api_resource.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = intercom 3 | 4 | [report] 5 | omit = 6 | */python?.?/* 7 | */site-packages/nose/* 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | venv 3 | .venv 4 | .coverage 5 | .env 6 | *.egg-info 7 | *.pyc 8 | .DS_Store 9 | htmlcov 10 | docs/_build 11 | 12 | intercom.sublime-project 13 | intercom.sublime-workspace 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - 2.7 5 | - 3.4 6 | - 3.5 7 | - 3.6 8 | install: 9 | - pip install -r requirements.txt 10 | - pip install -r dev-requirements.txt 11 | script: 12 | - nosetests --with-coverag tests/unit 13 | after_success: 14 | coveralls --verbose 15 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | python-intercom is written and maintained by John Keyes and various 2 | contributors: 3 | 4 | Development Lead 5 | ~~~~~~~~~~~~~~~~ 6 | 7 | - John Keyes `@jkeyes `_ 8 | 9 | Patches and Suggestions 10 | ~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | - `vrachnis `_ 13 | - `sdorazio `_ 14 | - `Cameron Maske `_ 15 | - `Martin-Zack Mekkaoui `_ 16 | - `Marsel Mavletkulov `_ 17 | - `Grant McConnaughey `_ 18 | - `Robert Elliott `_ 19 | - `Jared Morse `_ 20 | - `Rafael `_ 21 | - `jacoor `_ 22 | - `maiiku `_ 23 | - `Piotr Kilczuk `_ 24 | - `Forrest Scofield `_ 25 | - `Jordan Feldstein `_ 26 | - `François Voron `_ 27 | - `Gertjan Oude Lohuis `_ 28 | 29 | Intercom 30 | ~~~~~~~~ 31 | 32 | - `Darragh Curran `_ 33 | - `Bill de hÓra `_ 34 | - `Jeff Gardner `_ 35 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | * 3.1.0 4 | * Added support for the Scroll API. (`#156 `_) 5 | * 3.0.5 6 | * Increased default request timeout to 90 seconds. This can also be set by the `INTERCOM_REQUEST_TIMEOUT` environment variable. (`#154 `_) 7 | * 3.0.4 8 | * Added `resource_type` attribute to lightweight classes. (`#153 `_) 9 | * 3.0.3 10 | * Removed `count` API operation, this is supported via `client.counts` now. (`#152 `_) 11 | * 3.0.2 12 | * Added multipage support for Event.find_all. (`#147 `_) 13 | * 3.0.1 14 | * Added support for HTTP keep-alive. (`#146 `_) 15 | * 3.0 16 | * 3.0b4 17 | * Added conversation.mark_read method. (`#136 `_) 18 | * 3.0b3 19 | * Added TokenUnauthorizedError. (`#134 `_) 20 | * Added UTC datetime everywhere. (`#130 `_) 21 | * Fixed connection error when paginating. (`#125 `_) 22 | * Added Personal Access Token support. (`#123 `_) 23 | * Fixed links to Intercom API documentation. (`#115 `_) 24 | * 3.0b2 25 | * Added support for Leads. (`#113 `_) 26 | * Added support for Bulk API. (`#112 `_) 27 | * 3.0b1 28 | * Moved to new client based approach. (`#108 `_) 29 | * 2.1.1 30 | * No runtime changes. 31 | * 2.1.0 32 | * Adding interface support for opens, closes, and assignments of conversations. (`#101 `_) 33 | * Ensuring identity_hash only contains variables with valid values. (`#100 `_) 34 | * Adding support for unique_user_constraint and parameter_not_found errors. (`#97 `_) 35 | * 2.0.0 36 | * Added support for non-ASCII character sets. (`#86 `_) 37 | * Fixed response handling where no encoding is specified. (`#81 `_) 38 | * Added support for `None` values in `FlatStore`. (`#88 `_) 39 | * 2.0.beta 40 | * Fixed `UnboundLocalError` in `Request.parse_body`. (`#72 `_) 41 | * Added support for replies with an empty body. (`#72 `_) 42 | * Fixed a bug in identifying changed attributes when creating new resources. (`#77 `_) 43 | * 2.0.alpha 44 | * Added support for Intercom API v2. 45 | * Added support for Python 3. 46 | * 0.2.13 47 | * Fixed wildcard import from `intercom`. (`#28 `_) 48 | * 0.2.12 49 | * Added RTD theme to requirements.txt 50 | * 0.2.11 51 | * Added support for events. (`#25 `_) 52 | * Using RTD theme for documentation. 53 | * Fixed links to Intercom API docs. 54 | * 0.2.10 55 | * Added basic support for companies. (`#18 `_) 56 | * Fixed User.delete. (`#19 `_) 57 | * Fixed links to Intercom API docs. 58 | * Fixed doctests. 59 | * 0.2.9 60 | * Added `unsubscribed_from_emails` attribute to `User` object. (`#15 `_) 61 | * Added support for `last_request_at` parameter in `Intercom.create_user`. (`#16 `_) 62 | * Added support for page, per_page, tag_id, and tag_name parameters on `Intercom.get_users`. (`#17 `_) 63 | * 0.2.8 64 | * Added support for tagging. (`#13 `_) 65 | * Fixed installation into a clean python environment. (`#12 `_) 66 | * Fixed doctest. 67 | * Updated PEP8 formatting. 68 | * 0.2.7 69 | * Fixed delete user support to send bodyless request. 70 | * Added support for user notes. 71 | * 0.2.6 72 | * Added support for delete user. 73 | * 0.2.5 74 | * Fixed consistent version numbering (docs and code). 75 | * 0.2.4 76 | * Fixed handling of invalid JSON responses. 77 | * Fixed doctests to pass with current Intercom dummy API. 78 | * 0.2.3 79 | * Fixed version number of distribution to match documentation. 80 | * 0.2.2 81 | * Updated docstrings and doctests. 82 | * 0.2.1 83 | * Added some docstrings. 84 | * 0.2 85 | * Created source distribution. (`#2 `_) 86 | * Fixed error names. (`#1 `_) 87 | * 0.1 88 | * Initial release. 89 | -------------------------------------------------------------------------------- /INSTALL.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | The simplest way to install python-intercom is with `pip `_: 5 | 6 | :: 7 | 8 | pip install python-intercom 9 | 10 | or you may download a `.tar.gz` source archive from `pypi `_: 11 | 12 | :: 13 | 14 | tar xf python-intercom.tar.gz 15 | cd python-intercom 16 | python setup.py install 17 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | See http://jkeyes.mit-license.org/ for specific license information. 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | recursive-include docs *.rst 3 | include requirements.txt 4 | recursive-include tests *.py -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Development dependencies. 3 | # 4 | nose==1.3.4 5 | mock==1.0.1 6 | coveralls==0.5 7 | coverage==3.7.1 8 | sphinx==1.4.8 9 | sphinx-rtd-theme==0.1.9 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/intercom.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/intercom.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/intercom" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/intercom" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/api/intercom.api_operations.rst: -------------------------------------------------------------------------------- 1 | intercom.api_operations package 2 | =============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | intercom.api_operations.all module 8 | ---------------------------------- 9 | 10 | .. automodule:: intercom.api_operations.all 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | intercom.api_operations.count module 16 | ------------------------------------ 17 | 18 | .. automodule:: intercom.api_operations.count 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | intercom.api_operations.delete module 24 | ------------------------------------- 25 | 26 | .. automodule:: intercom.api_operations.delete 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | intercom.api_operations.find module 32 | ----------------------------------- 33 | 34 | .. automodule:: intercom.api_operations.find 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | intercom.api_operations.find_all module 40 | --------------------------------------- 41 | 42 | .. automodule:: intercom.api_operations.find_all 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | intercom.api_operations.load module 48 | ----------------------------------- 49 | 50 | .. automodule:: intercom.api_operations.load 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | intercom.api_operations.save module 56 | ----------------------------------- 57 | 58 | .. automodule:: intercom.api_operations.save 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: intercom.api_operations 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /docs/api/intercom.extended_api_operations.rst: -------------------------------------------------------------------------------- 1 | intercom.extended_api_operations package 2 | ======================================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | intercom.extended_api_operations.reply module 8 | --------------------------------------------- 9 | 10 | .. automodule:: intercom.extended_api_operations.reply 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | intercom.extended_api_operations.users module 16 | --------------------------------------------- 17 | 18 | .. automodule:: intercom.extended_api_operations.users 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: intercom.extended_api_operations 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/api/intercom.generic_handlers.rst: -------------------------------------------------------------------------------- 1 | intercom.generic_handlers package 2 | ================================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | intercom.generic_handlers.base_handler module 8 | --------------------------------------------- 9 | 10 | .. automodule:: intercom.generic_handlers.base_handler 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | intercom.generic_handlers.count module 16 | -------------------------------------- 17 | 18 | .. automodule:: intercom.generic_handlers.count 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | intercom.generic_handlers.tag module 24 | ------------------------------------ 25 | 26 | .. automodule:: intercom.generic_handlers.tag 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | intercom.generic_handlers.tag_find_all module 32 | --------------------------------------------- 33 | 34 | .. automodule:: intercom.generic_handlers.tag_find_all 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: intercom.generic_handlers 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/api/intercom.lib.rst: -------------------------------------------------------------------------------- 1 | intercom.lib package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | intercom.lib.flat_store module 8 | ------------------------------ 9 | 10 | .. automodule:: intercom.lib.flat_store 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | intercom.lib.setter_property module 16 | ----------------------------------- 17 | 18 | .. automodule:: intercom.lib.setter_property 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | intercom.lib.typed_json_deserializer module 24 | ------------------------------------------- 25 | 26 | .. automodule:: intercom.lib.typed_json_deserializer 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: intercom.lib 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/api/intercom.rst: -------------------------------------------------------------------------------- 1 | intercom package 2 | ================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | intercom.api_operations 10 | intercom.extended_api_operations 11 | intercom.generic_handlers 12 | intercom.lib 13 | intercom.traits 14 | 15 | Submodules 16 | ---------- 17 | 18 | intercom.admin module 19 | --------------------- 20 | 21 | .. automodule:: intercom.admin 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | 26 | intercom.collection_proxy module 27 | -------------------------------- 28 | 29 | .. automodule:: intercom.collection_proxy 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | intercom.company module 35 | ----------------------- 36 | 37 | .. automodule:: intercom.company 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | intercom.conversation module 43 | ---------------------------- 44 | 45 | .. automodule:: intercom.conversation 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | 50 | intercom.count module 51 | --------------------- 52 | 53 | .. automodule:: intercom.count 54 | :members: 55 | :undoc-members: 56 | :show-inheritance: 57 | 58 | intercom.errors module 59 | ---------------------- 60 | 61 | .. automodule:: intercom.errors 62 | :members: 63 | :undoc-members: 64 | :show-inheritance: 65 | 66 | intercom.event module 67 | --------------------- 68 | 69 | .. automodule:: intercom.event 70 | :members: 71 | :undoc-members: 72 | :show-inheritance: 73 | 74 | intercom.message module 75 | ----------------------- 76 | 77 | .. automodule:: intercom.message 78 | :members: 79 | :undoc-members: 80 | :show-inheritance: 81 | 82 | intercom.note module 83 | -------------------- 84 | 85 | .. automodule:: intercom.note 86 | :members: 87 | :undoc-members: 88 | :show-inheritance: 89 | 90 | intercom.notification module 91 | ---------------------------- 92 | 93 | .. automodule:: intercom.notification 94 | :members: 95 | :undoc-members: 96 | :show-inheritance: 97 | 98 | intercom.request module 99 | ----------------------- 100 | 101 | .. automodule:: intercom.request 102 | :members: 103 | :undoc-members: 104 | :show-inheritance: 105 | 106 | intercom.segment module 107 | ----------------------- 108 | 109 | .. automodule:: intercom.segment 110 | :members: 111 | :undoc-members: 112 | :show-inheritance: 113 | 114 | intercom.subscription module 115 | ---------------------------- 116 | 117 | .. automodule:: intercom.subscription 118 | :members: 119 | :undoc-members: 120 | :show-inheritance: 121 | 122 | intercom.tag module 123 | ------------------- 124 | 125 | .. automodule:: intercom.tag 126 | :members: 127 | :undoc-members: 128 | :show-inheritance: 129 | 130 | intercom.user module 131 | -------------------- 132 | 133 | .. automodule:: intercom.user 134 | :members: 135 | :undoc-members: 136 | :show-inheritance: 137 | 138 | intercom.utils module 139 | --------------------- 140 | 141 | .. automodule:: intercom.utils 142 | :members: 143 | :undoc-members: 144 | :show-inheritance: 145 | 146 | 147 | Module contents 148 | --------------- 149 | 150 | .. automodule:: intercom 151 | :members: 152 | :undoc-members: 153 | :show-inheritance: -------------------------------------------------------------------------------- /docs/api/intercom.traits.rst: -------------------------------------------------------------------------------- 1 | intercom.traits package 2 | ======================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | intercom.traits.api_resource module 8 | ----------------------------------- 9 | 10 | .. automodule:: intercom.traits.api_resource 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | intercom.traits.incrementable_attributes module 16 | ----------------------------------------------- 17 | 18 | .. automodule:: intercom.traits.incrementable_attributes 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: intercom.traits 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/api/modules.rst: -------------------------------------------------------------------------------- 1 | intercom 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | intercom 8 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # intercom documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Mar 20 09:38:03 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sphinx_rtd_theme 15 | import sys, os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | docs_dir = os.path.dirname(__file__) 22 | path_dir = os.path.abspath(os.path.join(docs_dir, '..')) 23 | sys.path.insert(0, path_dir) 24 | 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'python-intercom' 48 | from datetime import datetime 49 | now = datetime.now() 50 | copyright = u'%s, John Keyes' % (now.year) 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | 58 | import re 59 | with open(os.path.join(path_dir, 'intercom', '__init__.py')) as init: 60 | source = init.read() 61 | m = re.search("__version__ = '(.*)'", source, re.M) 62 | version = m.groups()[0] 63 | 64 | # The full version, including alpha/beta/rc tags. 65 | release = version 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | #language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all documents. 82 | #default_role = None 83 | 84 | # If true, '()' will be appended to :func: etc. cross-reference text. 85 | #add_function_parentheses = True 86 | 87 | # If true, the current module name will be prepended to all description 88 | # unit titles (such as .. function::). 89 | #add_module_names = True 90 | 91 | # If true, sectionauthor and moduleauthor directives will be shown in the 92 | # output. They are ignored by default. 93 | #show_authors = False 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | pygments_style = 'sphinx' 97 | 98 | # A list of ignored prefixes for module index sorting. 99 | #modindex_common_prefix = [] 100 | 101 | 102 | # -- Options for HTML output --------------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | try: 107 | import sphinx_rtd_theme 108 | html_theme = "sphinx_rtd_theme" 109 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 110 | except ImportError: 111 | html_theme = 'default' 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | #html_theme_options = {} 117 | 118 | # Add any paths that contain custom themes here, relative to this directory. 119 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 120 | 121 | # The name for this set of Sphinx documents. If None, it defaults to 122 | # " v documentation". 123 | #html_title = None 124 | 125 | # A shorter title for the navigation bar. Default is the same as html_title. 126 | #html_short_title = None 127 | 128 | # The name of an image file (relative to this directory) to place at the top 129 | # of the sidebar. 130 | #html_logo = None 131 | 132 | # The name of an image file (within the static path) to use as favicon of the 133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 134 | # pixels large. 135 | #html_favicon = None 136 | 137 | # Add any paths that contain custom static files (such as style sheets) here, 138 | # relative to this directory. They are copied after the builtin static files, 139 | # so a file named "default.css" will overwrite the builtin "default.css". 140 | # html_static_path = ['_static'] 141 | 142 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 143 | # using the given strftime format. 144 | #html_last_updated_fmt = '%b %d, %Y' 145 | 146 | # If true, SmartyPants will be used to convert quotes and dashes to 147 | # typographically correct entities. 148 | #html_use_smartypants = True 149 | 150 | # Custom sidebar templates, maps document names to template names. 151 | #html_sidebars = {} 152 | 153 | # Additional templates that should be rendered to pages, maps page names to 154 | # template names. 155 | #html_additional_pages = {} 156 | 157 | # If false, no module index is generated. 158 | #html_domain_indices = True 159 | 160 | # If false, no index is generated. 161 | #html_use_index = True 162 | 163 | # If true, the index is split into individual pages for each letter. 164 | #html_split_index = False 165 | 166 | # If true, links to the reST sources are added to the pages. 167 | #html_show_sourcelink = True 168 | 169 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 170 | #html_show_sphinx = True 171 | 172 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 173 | #html_show_copyright = True 174 | 175 | # If true, an OpenSearch description file will be output, and all pages will 176 | # contain a tag referring to it. The value of this option must be the 177 | # base URL from which the finished HTML is served. 178 | #html_use_opensearch = '' 179 | 180 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 181 | #html_file_suffix = None 182 | 183 | # Output file base name for HTML help builder. 184 | htmlhelp_basename = 'intercomdoc' 185 | 186 | 187 | # -- Options for LaTeX output -------------------------------------------------- 188 | 189 | latex_elements = { 190 | # The paper size ('letterpaper' or 'a4paper'). 191 | #'papersize': 'letterpaper', 192 | 193 | # The font size ('10pt', '11pt' or '12pt'). 194 | #'pointsize': '10pt', 195 | 196 | # Additional stuff for the LaTeX preamble. 197 | #'preamble': '', 198 | } 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, author, documentclass [howto/manual]). 202 | latex_documents = [ 203 | ('index', 'python-intercom.tex', u'python-intercom Documentation', 204 | u'John Keyes', 'manual'), 205 | ] 206 | 207 | # The name of an image file (relative to this directory) to place at the top of 208 | # the title page. 209 | #latex_logo = None 210 | 211 | # For "manual" documents, if this is true, then toplevel headings are parts, 212 | # not chapters. 213 | #latex_use_parts = False 214 | 215 | # If true, show page references after internal links. 216 | #latex_show_pagerefs = False 217 | 218 | # If true, show URL addresses after external links. 219 | #latex_show_urls = False 220 | 221 | # Documents to append as an appendix to all manuals. 222 | #latex_appendices = [] 223 | 224 | # If false, no module index is generated. 225 | #latex_domain_indices = True 226 | 227 | 228 | # -- Options for manual page output -------------------------------------------- 229 | 230 | # One entry per manual page. List of tuples 231 | # (source start file, name, description, authors, manual section). 232 | man_pages = [ 233 | ('index', 'python-intercom', u'python-intercom Documentation', 234 | [u'John Keyes'], 1) 235 | ] 236 | 237 | # If true, show URL addresses after external links. 238 | #man_show_urls = False 239 | 240 | 241 | # -- Options for Texinfo output ------------------------------------------------ 242 | 243 | # Grouping the document tree into Texinfo files. List of tuples 244 | # (source start file, target name, title, author, 245 | # dir menu entry, description, category) 246 | texinfo_documents = [ 247 | ('index', 'python-intercom', u'python-intercom Documentation', 248 | u'John Keyes', 'python-intercom', 'One line description of project.', 249 | 'Miscellaneous'), 250 | ] 251 | 252 | # Documents to append as an appendix to all manuals. 253 | #texinfo_appendices = [] 254 | 255 | # If false, no module index is generated. 256 | #texinfo_domain_indices = True 257 | 258 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 259 | #texinfo_show_urls = 'footnote' 260 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | Running the tests 5 | ----------------- 6 | 7 | Run the unit tests: 8 | 9 | :: 10 | 11 | nosetests tests/unit 12 | 13 | Run the integration tests: 14 | 15 | :: 16 | 17 | # THESE SHOULD ONLY BE RUN ON A TEST APP! 18 | INTERCOM_PERSONAL_ACCESS_TOKEN=xxx nosetests tests/integration 19 | 20 | Generate the Documentation 21 | -------------------------- 22 | 23 | :: 24 | 25 | cd docs 26 | make html 27 | 28 | Code coverage 29 | ------------- 30 | 31 | Generate a code coverage report: 32 | 33 | :: 34 | 35 | nosetests --with-coverage --cover-package=intercom tests/unit 36 | 37 | 38 | Runtime Dependencies 39 | -------------------- 40 | 41 | * `Requests `_ – an HTTP library “for human beings” 42 | * `inflection `_ – Inflection is a string transformation library. It singularizes and pluralizes English words, and transforms strings from CamelCase to underscored string. 43 | * `six `_ – Six is a Python 2 and 3 compatibility library. It provides utility functions for smoothing over the differences between the Python versions with the goal of writing Python code that is compatible on both Python versions. 44 | * `certifi `_ – Certifi is a carefully curated collection of Root Certificates for validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts. 45 | * `pytz `_ – pytz brings the Olson tz database into Python. This library allows accurate and cross platform timezone calculations. It also solves the issue of ambiguous times at the end of daylight saving time. 46 | 47 | Development Dependencies 48 | ------------------------ 49 | 50 | * `nose `_ – makes unit testing easier. 51 | * `coverage `_ – code coverage. 52 | * `mock `_ – patching methods for unit testing. 53 | * `Sphinx `_ – documentation decorator. 54 | * `Sphinx theme for readthedocs.org `_ – theme for the documentation. 55 | 56 | Authors 57 | ------- 58 | 59 | .. include:: ../AUTHORS.rst 60 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | How do I start a session? 5 | ------------------------- 6 | 7 | :: 8 | 9 | user = intercom.users.create(email='bingo@example.com') 10 | # register a new session 11 | user.new_session = True 12 | intercom.users.save(user) 13 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | python-intercom 3 | =============== 4 | 5 | .. toctree:: 6 | :hidden: 7 | 8 | installation 9 | faq 10 | changelog 11 | development 12 | api/modules 13 | 14 | Installation 15 | ============ 16 | 17 | Stable releases of python-intercom can be installed with 18 | `pip `_ or you may download a `.tgz` source 19 | archive from `pypi `_. 20 | See the :doc:`installation` page for more detailed instructions. 21 | 22 | If you want to use the latest code, you can grab it from our 23 | `Git repository `_, or `fork it `_. 24 | 25 | Usage 26 | =================================== 27 | 28 | Authorization 29 | ------------- 30 | 31 | Intercom documentation: `Personal Access Tokens `_. 32 | 33 | :: 34 | 35 | from intercom.client import Client 36 | intercom = Client(personal_access_token='my_personal_access_token') 37 | 38 | Users 39 | ----- 40 | 41 | Create or Update User 42 | +++++++++++++++++++++ 43 | 44 | Intercom documentation: `Create or Update Users `_. 45 | 46 | :: 47 | 48 | intercom.users.create(user_id='1234', email='bob@example.com') 49 | 50 | Updating the Last Seen Time 51 | +++++++++++++++++++++++++++ 52 | 53 | Intercom documentation: `Updating the Last Seen Time `_. 54 | 55 | :: 56 | 57 | user = intercom.users.create(used_id='25', last_request_at=datetime.utcnow()) 58 | 59 | List Users 60 | ++++++++++ 61 | 62 | Intercom documentation: `List Users `_. 63 | 64 | :: 65 | 66 | for user in intercom.users.all(): 67 | ... 68 | 69 | List by Tag, Segment, Company 70 | +++++++++++++++++++++++++++++ 71 | 72 | Intercom documentation: `List by Tag, Segment, Company `_. 73 | 74 | :: 75 | 76 | # tag request 77 | intercom.users.find_all(tag_id='30126') 78 | 79 | # segment request 80 | intercom.users.find_all(segment_id='30126') 81 | 82 | 83 | View a User 84 | +++++++++++ 85 | 86 | Intercom documentation: `View a User `_. 87 | 88 | :: 89 | 90 | # ID request 91 | intercom.users.find(id='1') 92 | 93 | # User ID request 94 | intercom.users.find(user_id='1') 95 | 96 | # Email request 97 | intercom.users.find(email='bob@example.com') 98 | 99 | Delete a User 100 | +++++++++++++ 101 | 102 | Intercom documentation: `Deleting a User `_. 103 | 104 | :: 105 | 106 | # ID Delete Request 107 | user = intercom.users.find(id='1') 108 | deleted_user = intercom.users.delete(user) 109 | 110 | # User ID Delete Request 111 | user = intercom.users.find(user_id='1') 112 | deleted_user = intercom.users.delete(user) 113 | 114 | # Email Delete Request 115 | user = intercom.users.find(email='bob@example.com') 116 | deleted_user = intercom.users.delete(user) 117 | 118 | 119 | Companies 120 | --------- 121 | 122 | Create or Update Company 123 | ++++++++++++++++++++++++ 124 | 125 | Intercom documentation: `Create or Update Company `_. 126 | 127 | :: 128 | 129 | intercom.companies.create(company_id=6, name="Blue Sun", plan="Paid") 130 | 131 | List Companies 132 | ++++++++++++++ 133 | 134 | Intercom documentation: `List Companies `_. 135 | 136 | :: 137 | 138 | for company in intercom.companies.all(): 139 | ... 140 | 141 | List by Tag or Segment 142 | ++++++++++++++++++++++ 143 | 144 | Intercom documentation: `List by Tag or Segment `_. 145 | 146 | :: 147 | 148 | # tag request 149 | intercom.companies.find(tag_id="1234") 150 | 151 | # segment request 152 | intercom.companies.find(segment_id="4567") 153 | 154 | View a Company 155 | ++++++++++++++ 156 | 157 | Intercom documentation: `View a Company `_. 158 | 159 | :: 160 | 161 | intercom.companies.find(id="41e66f0313708347cb0000d0") 162 | 163 | List Company Users 164 | ++++++++++++++++++ 165 | 166 | Intercom documentation: `List Company Users `_. 167 | 168 | :: 169 | 170 | company = intercom.companies.find(id="41e66f0313708347cb0000d0") 171 | for user in company.users: 172 | ... 173 | 174 | Admins 175 | ------ 176 | 177 | List Admins 178 | +++++++++++ 179 | 180 | Intercom documentation: `List Admins `_. 181 | 182 | :: 183 | 184 | for admin in intercom.admins.all(): 185 | ... 186 | 187 | Tags 188 | ---- 189 | 190 | Create and Update Tags 191 | ++++++++++++++++++++++ 192 | 193 | Intercom documentation: `Create and Update Tags `_. 194 | 195 | :: 196 | 197 | # Create Request 198 | tag = intercom.tags.create(name='Independentt') 199 | 200 | # Update Request 201 | intercom.tags.tag(name='Independent', id=tag.id) 202 | 203 | 204 | Tag or Untag Users & Companies 205 | ++++++++++++++++++++++++++++++ 206 | 207 | Intercom documentation: `Tag or Untag Users & Companies `_. 208 | 209 | :: 210 | 211 | # Multi-User Tag Request 212 | intercom.tags.tag(name='Independent', users=["42ea2f1b93891f6a99000427", "42ea2f1b93891f6a99000428"]) 213 | 214 | # Untag Request 215 | intercom.tags.untag(name='blue', users=["42ea2f1b93891f6a99000427"]) 216 | 217 | 218 | List Tags for an App 219 | ++++++++++++++++++++ 220 | 221 | Intercom Documentation: `List Tags for an App `_. 222 | 223 | :: 224 | 225 | for intercom.tags in Tag.all(): 226 | ... 227 | 228 | Segments 229 | -------- 230 | 231 | List Segments 232 | +++++++++++++ 233 | 234 | Intercom Documentation: `List Segments `_. 235 | 236 | :: 237 | 238 | for segment in intercom.segments.all(): 239 | ... 240 | 241 | View a Segment 242 | ++++++++++++++ 243 | 244 | Intercom Documentation: `View a Segment `_. 245 | 246 | :: 247 | 248 | intercom.segments.find(id='1234') 249 | 250 | Notes 251 | ----- 252 | 253 | Create a Note 254 | +++++++++++++ 255 | 256 | Intercom documentation: `Create a Note `_. 257 | 258 | :: 259 | 260 | intercom.notes.create(email="joe@exampe.com", body="Text for the note") 261 | 262 | 263 | List Notes for a User 264 | +++++++++++++++++++++ 265 | 266 | Intercom documentation: `List Notes for a User `_. 267 | 268 | :: 269 | 270 | # User ID Request 271 | for note in intercom.notes.find_all(user_id='123'): 272 | ... 273 | 274 | # User Email Request 275 | for note in intercom.notes.find_all(email='foo@bar.com'): 276 | ... 277 | 278 | View a Note 279 | +++++++++++ 280 | 281 | Intercom documentation: `View a Note `_. 282 | 283 | :: 284 | 285 | intercom.notes.find(id='1234') 286 | 287 | Events 288 | ------ 289 | 290 | Submitting Events 291 | +++++++++++++++++ 292 | 293 | Intercom documentation: `Submitting Events `_. 294 | 295 | :: 296 | 297 | intercom.events.create(event_name="Eventful 1", email=user.email, created_at=1403001013) 298 | 299 | 300 | Counts 301 | ------ 302 | 303 | Getting counts 304 | ++++++++++++++ 305 | 306 | Intercom documentation: `Getting Counts `_. 307 | 308 | :: 309 | 310 | # Conversation Admin Count 311 | intercom.counts.for_type(type='conversation', count='admin') 312 | 313 | # User Tag Count 314 | intercom.counts.for_type(type='user', count='tag') 315 | 316 | # User Segment Count 317 | intercom.counts.for_type(type='user', count='segment') 318 | 319 | # Company Tag Count 320 | intercom.counts.for_type(type='company', count='tag') 321 | 322 | # Company User Count 323 | intercom.counts.for_type(type='company', count='user') 324 | 325 | # Global App Counts 326 | intercom.counts.for_type() 327 | 328 | Conversations 329 | ------------- 330 | 331 | Admin Initiated Conversation 332 | ++++++++++++++++++++++++++++ 333 | 334 | Intercom documentation: `Admin Initiated Conversation `_. 335 | 336 | :: 337 | 338 | message_data = { 339 | 'message_type': 'email', 340 | 'subject': 'This Land', 341 | 'body': "Har har har! Mine is an evil laugh!", 342 | 'template': "plain", 343 | 'from': { 344 | 'type': "admin", 345 | 'id': "394051" 346 | }, 347 | 'to': { 348 | 'type': "user", 349 | 'id': "536e564f316c83104c000020" 350 | } 351 | } 352 | intercom.messages.create(**message_data) 353 | 354 | User Initiated Conversation 355 | +++++++++++++++++++++++++++ 356 | 357 | Intercom documentation: `User Initiated Conversation `_. 358 | 359 | :: 360 | 361 | message_data = { 362 | 'from': { 363 | 'type': "user", 364 | 'id': "536e564f316c83104c000020" 365 | }, 366 | 'body': "Hey" 367 | } 368 | intercom.messages.create(**message_data) 369 | 370 | List Conversations 371 | ++++++++++++++++++ 372 | 373 | Intercom documentation: `List Conversations `_. 374 | 375 | :: 376 | 377 | intercom.conversations.find_all(type='admin', id=25, open=True) 378 | 379 | 380 | Get a Single Conversation 381 | +++++++++++++++++++++++++ 382 | 383 | Intercom documentation: `Get a Single Conversation `_. 384 | 385 | :: 386 | 387 | intercom.conversations.find(id='147') 388 | 389 | Replying to a Conversation 390 | ++++++++++++++++++++++++++ 391 | 392 | Intercom documentation: `Replying to a Conversation `_. 393 | 394 | :: 395 | 396 | conversation.reply(type='user', email='bob@example.com', message_type='comment', body='foo') 397 | 398 | 399 | Marking a Conversation as Read 400 | ++++++++++++++++++++++++++++++ 401 | 402 | Intercom documentation: `Marking a Conversation as Read `_. 403 | 404 | :: 405 | 406 | conversation.read = True 407 | conversation.save() 408 | 409 | 410 | Webhooks and Notifications 411 | -------------------------- 412 | 413 | Manage Subscriptions 414 | ++++++++++++++++++++ 415 | 416 | Intercom documentation: `Manage Subscriptions `_. 417 | 418 | :: 419 | 420 | intercom.subscriptions.create(service_type='web', url='http://example.com', topics=['all']) 421 | 422 | 423 | View a Subscription 424 | +++++++++++++++++++ 425 | 426 | Intercom documentation: `View a Subscription `_. 427 | 428 | :: 429 | 430 | intercom.subscriptions.find(id='123') 431 | 432 | List Subscriptions 433 | ++++++++++++++++++ 434 | 435 | Intercom documentation: `List Subscriptions `_. 436 | 437 | :: 438 | 439 | for subscription in intercom.subscriptions.all(): 440 | ... 441 | 442 | Development 443 | =========== 444 | 445 | Our :doc:`development` page has detailed instructions on how to run our 446 | tests, and to produce coverage and pylint reports. 447 | 448 | Changelog 449 | ========= 450 | 451 | The :doc:`changelog` keeps track of changes per release. 452 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../INSTALL.rst 2 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\intercom.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\intercom.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /intercom/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # from datetime import datetime 4 | from .errors import (ArgumentError, AuthenticationError, # noqa 5 | BadGatewayError, BadRequestError, HttpError, IntercomError, 6 | MultipleMatchingUsersError, RateLimitExceeded, ResourceNotFound, 7 | ServerError, ServiceUnavailableError, UnexpectedError, TokenUnauthorizedError) 8 | 9 | __version__ = '3.1.0' 10 | 11 | 12 | RELATED_DOCS_TEXT = "See https://github.com/jkeyes/python-intercom \ 13 | for usage examples." 14 | COMPATIBILITY_WARNING_TEXT = "It looks like you are upgrading from \ 15 | an older version of python-intercom. Please note that this new version \ 16 | (%s) is not backwards compatible." % (__version__) 17 | COMPATIBILITY_WORKAROUND_TEXT = "To get rid of this error please set \ 18 | Intercom.app_api_key and don't set Intercom.api_key." 19 | CONFIGURATION_REQUIRED_TEXT = "You must set both Intercom.app_id and \ 20 | Intercom.app_api_key to use this client." 21 | -------------------------------------------------------------------------------- /intercom/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Admin(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/api_operations/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Package for operations that can be performed on a resource.""" 3 | -------------------------------------------------------------------------------- /intercom/api_operations/all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to retrieve all instances of a particular resource.""" 3 | 4 | from intercom import utils 5 | from intercom.collection_proxy import CollectionProxy 6 | 7 | 8 | class All(object): 9 | """A mixin that provides `all` functionality.""" 10 | 11 | def all(self): 12 | """Return a CollectionProxy for the resource.""" 13 | collection = utils.resource_class_to_collection_name( 14 | self.collection_class) 15 | finder_url = "/%s" % (collection) 16 | return CollectionProxy( 17 | self.client, self.collection_class, collection, finder_url) 18 | -------------------------------------------------------------------------------- /intercom/api_operations/bulk.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Support for the Intercom Bulk API. 3 | 4 | Ref: https://developers.intercom.io/reference#bulk-apis 5 | """ 6 | 7 | from intercom import utils 8 | 9 | 10 | def item_for_api(method, data_type, item): 11 | """Return a Bulk API item.""" 12 | return { 13 | 'method': method, 14 | 'data_type': data_type, 15 | 'data': item 16 | } 17 | 18 | 19 | class Submit(object): 20 | """Provide Bulk API support to subclasses.""" 21 | 22 | def submit_bulk_job(self, create_items=[], delete_items=[], job_id=None): 23 | """Submit a Bulk API job.""" 24 | from intercom import event 25 | from intercom.errors import HttpError 26 | from intercom.job import Job 27 | 28 | if self.collection_class == event.Event and delete_items: 29 | raise Exception("Events do not support bulk delete operations.") 30 | data_type = utils.resource_class_to_name(self.collection_class) 31 | collection_name = utils.resource_class_to_collection_name(self.collection_class) 32 | create_items = [item_for_api('post', data_type, item) for item in create_items] 33 | delete_items = [item_for_api('delete', data_type, item) for item in delete_items] 34 | 35 | bulk_request = { 36 | 'items': create_items + delete_items 37 | } 38 | if job_id: 39 | bulk_request['job'] = {'id': job_id} 40 | 41 | response = self.client.post('/bulk/%s' % (collection_name), bulk_request) 42 | if not response: 43 | raise HttpError('HTTP Error - No response entity returned.') 44 | return Job().from_response(response) 45 | 46 | 47 | class LoadErrorFeed(object): 48 | """Provide access to Bulk API error feed for a specific job.""" 49 | 50 | def errors(self, id): 51 | """Return errors for the Bulk API job specified.""" 52 | from intercom.errors import HttpError 53 | from intercom.job import Job 54 | response = self.client.get("/jobs/%s/error" % (id), {}) 55 | if not response: 56 | raise HttpError('Http Error - No response entity returned.') 57 | return Job.from_api(response) 58 | -------------------------------------------------------------------------------- /intercom/api_operations/convert.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to convert a contact into a user.""" 3 | 4 | 5 | class Convert(object): 6 | """A mixin that provides `convert` functionality.""" 7 | 8 | def convert(self, contact, user): 9 | """Convert the specified contact into the specified user.""" 10 | self.client.post( 11 | '/contacts/convert', 12 | { 13 | 'contact': {'user_id': contact.user_id}, 14 | 'user': self.identity_hash(user) 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /intercom/api_operations/delete.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to delete an instance of a particular resource.""" 3 | 4 | from intercom import utils 5 | 6 | 7 | class Delete(object): 8 | """A mixin that provides `delete` functionality.""" 9 | 10 | def delete(self, obj): 11 | """Delete the specified instance of this resource.""" 12 | collection = utils.resource_class_to_collection_name( 13 | self.collection_class) 14 | self.client.delete("/%s/%s" % (collection, obj.id), {}) 15 | return obj 16 | -------------------------------------------------------------------------------- /intercom/api_operations/find.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to find an instance of a particular resource.""" 3 | 4 | from intercom import HttpError 5 | from intercom import utils 6 | 7 | 8 | class Find(object): 9 | """A mixin that provides `find` functionality.""" 10 | 11 | def find(self, **params): 12 | """Find the instance of the resource based on the supplied parameters.""" 13 | collection = utils.resource_class_to_collection_name( 14 | self.collection_class) 15 | if 'id' in params: 16 | response = self.client.get( 17 | "/%s/%s" % (collection, params['id']), {}) 18 | else: 19 | response = self.client.get("/%s" % (collection), params) 20 | 21 | if response is None: 22 | raise HttpError('Http Error - No response entity returned') 23 | 24 | return self.collection_class(**response) 25 | -------------------------------------------------------------------------------- /intercom/api_operations/find_all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to find all instances of a particular resource.""" 3 | 4 | from intercom import utils 5 | from intercom.collection_proxy import CollectionProxy 6 | 7 | 8 | class FindAll(object): 9 | """A mixin that provides `find_all` functionality.""" 10 | 11 | proxy_class = CollectionProxy 12 | 13 | def find_all(self, **params): 14 | """Find all instances of the resource based on the supplied parameters.""" 15 | collection = utils.resource_class_to_collection_name( 16 | self.collection_class) 17 | if 'id' in params and 'type' not in params: 18 | finder_url = "/%s/%s" % (collection, params['id']) 19 | else: 20 | finder_url = "/%s" % (collection) 21 | finder_params = params 22 | return self.proxy_class( 23 | self.client, self.collection_class, collection, 24 | finder_url, finder_params) 25 | -------------------------------------------------------------------------------- /intercom/api_operations/load.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to load an instance of a particular resource.""" 3 | 4 | from intercom import HttpError 5 | from intercom import utils 6 | 7 | 8 | class Load(object): 9 | """A mixin that provides `load` functionality.""" 10 | 11 | def load(self, resource): 12 | """Load the resource from the latest data in Intercom.""" 13 | collection = utils.resource_class_to_collection_name( 14 | self.collection_class) 15 | if hasattr(resource, 'id'): 16 | response = self.client.get("/%s/%s" % (collection, resource.id), {}) # noqa 17 | else: 18 | raise Exception( 19 | "Cannot load %s as it does not have a valid id." % ( 20 | self.collection_class)) 21 | 22 | if response is None: 23 | raise HttpError('Http Error - No response entity returned') 24 | 25 | return resource.from_response(response) 26 | -------------------------------------------------------------------------------- /intercom/api_operations/save.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to create or save an instance of a particular resource.""" 3 | 4 | from intercom import utils 5 | 6 | 7 | class Save(object): 8 | """A mixin that provides `create` and `save` functionality.""" 9 | 10 | def create(self, **params): 11 | """Create an instance of the resource from the supplied parameters.""" 12 | collection = utils.resource_class_to_collection_name( 13 | self.collection_class) 14 | response = self.client.post("/%s/" % (collection), params) 15 | if response: # may be empty if we received a 202 16 | return self.collection_class(**response) 17 | 18 | def save(self, obj): 19 | """Save the instance of the resource.""" 20 | collection = utils.resource_class_to_collection_name( 21 | obj.__class__) 22 | params = obj.attributes 23 | if self.id_present(obj) and not self.posted_updates(obj): 24 | # update 25 | response = self.client.put('/%s/%s' % (collection, obj.id), params) 26 | else: 27 | # create 28 | params.update(self.identity_hash(obj)) 29 | response = self.client.post('/%s' % (collection), params) 30 | if response: 31 | return obj.from_response(response) 32 | 33 | def id_present(self, obj): 34 | """Return whether the obj has an `id` attribute with a value.""" 35 | return getattr(obj, 'id', None) and obj.id != "" 36 | 37 | def posted_updates(self, obj): 38 | """Return whether the updates to this object have been posted to Intercom.""" 39 | return getattr(obj, 'update_verb', None) == 'post' 40 | 41 | def identity_hash(self, obj): 42 | """Return the identity_hash for this object.""" 43 | identity_vars = getattr(obj, 'identity_vars', []) 44 | parts = {} 45 | for var in identity_vars: 46 | id_var = getattr(obj, var, None) 47 | if id_var: # only present id var if it is not blank or None 48 | parts[var] = id_var 49 | return parts 50 | -------------------------------------------------------------------------------- /intercom/api_operations/scroll.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to scroll through users.""" 3 | 4 | from intercom import utils 5 | from intercom.scroll_collection_proxy import ScrollCollectionProxy 6 | 7 | 8 | class Scroll(object): 9 | """A mixin that provides `scroll` functionality.""" 10 | 11 | def scroll(self, **params): 12 | """Find all instances of the resource based on the supplied parameters.""" 13 | collection_name = utils.resource_class_to_collection_name( 14 | self.collection_class) 15 | finder_url = "/{}/scroll".format(collection_name) 16 | return ScrollCollectionProxy( 17 | self.client, self.collection_class, collection_name, finder_url) 18 | -------------------------------------------------------------------------------- /intercom/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import requests 4 | 5 | 6 | class Client(object): 7 | 8 | def __init__(self, personal_access_token='my_personal_access_token'): 9 | self.personal_access_token = personal_access_token 10 | self.base_url = 'https://api.intercom.io' 11 | self.rate_limit_details = {} 12 | self.http_session = requests.Session() 13 | 14 | @property 15 | def _auth(self): 16 | return (self.personal_access_token, '') 17 | 18 | @property 19 | def admins(self): 20 | from intercom.service import admin 21 | return admin.Admin(self) 22 | 23 | @property 24 | def companies(self): 25 | from intercom.service import company 26 | return company.Company(self) 27 | 28 | @property 29 | def conversations(self): 30 | from intercom.service import conversation 31 | return conversation.Conversation(self) 32 | 33 | @property 34 | def counts(self): 35 | from intercom.service import count 36 | return count.Count(self) 37 | 38 | @property 39 | def events(self): 40 | from intercom.service import event 41 | return event.Event(self) 42 | 43 | @property 44 | def messages(self): 45 | from intercom.service import message 46 | return message.Message(self) 47 | 48 | @property 49 | def notes(self): 50 | from intercom.service import note 51 | return note.Note(self) 52 | 53 | @property 54 | def segments(self): 55 | from intercom.service import segment 56 | return segment.Segment(self) 57 | 58 | @property 59 | def subscriptions(self): 60 | from intercom.service import subscription 61 | return subscription.Subscription(self) 62 | 63 | @property 64 | def tags(self): 65 | from intercom.service import tag 66 | return tag.Tag(self) 67 | 68 | @property 69 | def users(self): 70 | from intercom.service import user 71 | return user.User(self) 72 | 73 | @property 74 | def leads(self): 75 | from intercom.service import lead 76 | return lead.Lead(self) 77 | 78 | @property 79 | def jobs(self): 80 | from intercom.service import job 81 | return job.Job(self) 82 | 83 | def _execute_request(self, request, params): 84 | result = request.execute(self.base_url, self._auth, params) 85 | self.rate_limit_details = request.rate_limit_details 86 | return result 87 | 88 | def get(self, path, params): 89 | from intercom import request 90 | req = request.Request('GET', path, self.http_session) 91 | return self._execute_request(req, params) 92 | 93 | def post(self, path, params): 94 | from intercom import request 95 | req = request.Request('POST', path, self.http_session) 96 | return self._execute_request(req, params) 97 | 98 | def put(self, path, params): 99 | from intercom import request 100 | req = request.Request('PUT', path, self.http_session) 101 | return self._execute_request(req, params) 102 | 103 | def delete(self, path, params): 104 | from intercom import request 105 | req = request.Request('DELETE', path, self.http_session) 106 | return self._execute_request(req, params) 107 | -------------------------------------------------------------------------------- /intercom/collection_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import six 4 | from intercom import HttpError 5 | from intercom import utils 6 | 7 | 8 | class CollectionProxy(six.Iterator): 9 | 10 | def __init__( 11 | self, client, collection_cls, collection, 12 | finder_url, finder_params={}): 13 | 14 | self.client = client 15 | 16 | # resource name 17 | self.resource_name = utils.resource_class_to_collection_name(collection_cls) 18 | 19 | # resource class 20 | self.resource_class = collection_cls 21 | 22 | # needed to create class instances of the resource 23 | self.collection_cls = collection_cls 24 | 25 | # needed to reference the collection in the response 26 | self.collection = collection 27 | 28 | # the original URL to retrieve the resources 29 | self.finder_url = finder_url 30 | 31 | # the params to filter the resources 32 | self.finder_params = finder_params 33 | 34 | # an iterator over the resources found in the response 35 | self.resources = None 36 | 37 | # a link to the next page of results 38 | self.next_page = None 39 | 40 | def __iter__(self): 41 | return self 42 | 43 | def __next__(self): 44 | if self.resources is None: 45 | # get the first page of results 46 | self.get_first_page() 47 | 48 | # try to get a resource if there are no more in the 49 | # current resource iterator (StopIteration is raised) 50 | # try to get the next page of results first 51 | try: 52 | resource = six.next(self.resources) 53 | except StopIteration: 54 | self.get_next_page() 55 | resource = six.next(self.resources) 56 | 57 | instance = self.collection_cls(**resource) 58 | return instance 59 | 60 | def __getitem__(self, index): 61 | for i in range(index): 62 | six.next(self) 63 | return six.next(self) 64 | 65 | def get_first_page(self): 66 | # get the first page of results 67 | return self.get_page(self.finder_url, self.finder_params) 68 | 69 | def get_next_page(self): 70 | # get the next page of results 71 | return self.get_page(self.next_page) 72 | 73 | def get_page(self, url, params={}): 74 | # get a page of results 75 | # from intercom import Intercom 76 | 77 | # if there is no url stop iterating 78 | if url is None: 79 | raise StopIteration 80 | 81 | response = self.client.get(url, params) 82 | if response is None: 83 | raise HttpError('Http Error - No response entity returned') 84 | 85 | collection = response[self.collection] 86 | # if there are no resources in the response stop iterating 87 | if collection is None: 88 | raise StopIteration 89 | 90 | # create the resource iterator 91 | self.resources = iter(collection) 92 | # grab the next page URL if one exists 93 | self.next_page = self.extract_next_link(response) 94 | 95 | def paging_info_present(self, response): 96 | return 'pages' in response and 'type' in response['pages'] 97 | 98 | def extract_next_link(self, response): 99 | if self.paging_info_present(response): 100 | paging_info = response["pages"] 101 | if paging_info["next"]: 102 | next_parsed = six.moves.urllib.parse.urlparse(paging_info["next"]) 103 | return '{}?{}'.format(next_parsed.path, next_parsed.query) 104 | -------------------------------------------------------------------------------- /intercom/company.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Company(Resource): 7 | update_verb = 'post' 8 | identity_vars = ['id', 'company_id'] 9 | 10 | @property 11 | def flat_store_attributes(self): 12 | return ['custom_attributes'] 13 | -------------------------------------------------------------------------------- /intercom/conversation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Collection module for Conversations.""" 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Conversation(Resource): 7 | """Collection class for Converations.""" 8 | -------------------------------------------------------------------------------- /intercom/count.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Count Resource.""" 3 | 4 | from intercom.traits.api_resource import Resource 5 | 6 | 7 | class Count(Resource): 8 | """Collection class for Counts.""" 9 | -------------------------------------------------------------------------------- /intercom/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class IntercomError(Exception): 5 | 6 | def __init__(self, message=None, context=None): 7 | super(IntercomError, self).__init__(message) 8 | self.message = message 9 | self.context = context 10 | 11 | 12 | class ArgumentError(ValueError, IntercomError): 13 | pass 14 | 15 | 16 | class HttpError(IntercomError): 17 | pass 18 | 19 | 20 | class ResourceNotFound(IntercomError): 21 | pass 22 | 23 | 24 | class AuthenticationError(IntercomError): 25 | pass 26 | 27 | 28 | class ServerError(IntercomError): 29 | pass 30 | 31 | 32 | class BadGatewayError(IntercomError): 33 | pass 34 | 35 | 36 | class ServiceUnavailableError(IntercomError): 37 | pass 38 | 39 | 40 | class BadRequestError(IntercomError): 41 | pass 42 | 43 | 44 | class RateLimitExceeded(IntercomError): 45 | pass 46 | 47 | 48 | class ResourceNotRestorable(IntercomError): 49 | pass 50 | 51 | 52 | class MultipleMatchingUsersError(IntercomError): 53 | pass 54 | 55 | 56 | class UnexpectedError(IntercomError): 57 | pass 58 | 59 | 60 | class TokenUnauthorizedError(IntercomError): 61 | pass 62 | 63 | 64 | class TokenNotFoundError(IntercomError): 65 | pass 66 | 67 | 68 | error_codes = { 69 | 'unauthorized': AuthenticationError, 70 | 'forbidden': AuthenticationError, 71 | 'bad_request': BadRequestError, 72 | 'action_forbidden': BadRequestError, 73 | 'missing_parameter': BadRequestError, 74 | 'parameter_invalid': BadRequestError, 75 | 'parameter_not_found': BadRequestError, 76 | 'client_error': BadRequestError, 77 | 'type_mismatch': BadRequestError, 78 | 'not_found': ResourceNotFound, 79 | 'admin_not_found': ResourceNotFound, 80 | 'not_restorable': ResourceNotRestorable, 81 | 'rate_limit_exceeded': RateLimitExceeded, 82 | 'service_unavailable': ServiceUnavailableError, 83 | 'server_error': ServiceUnavailableError, 84 | 'conflict': MultipleMatchingUsersError, 85 | 'unique_user_constraint': MultipleMatchingUsersError, 86 | 'token_unauthorized': TokenUnauthorizedError, 87 | 'token_not_found': TokenNotFoundError, 88 | 'token_revoked': TokenNotFoundError, 89 | 'token_blocked': TokenNotFoundError, 90 | 'token_expired': TokenNotFoundError 91 | } 92 | -------------------------------------------------------------------------------- /intercom/event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Event(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/extended_api_operations/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /intercom/extended_api_operations/tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to return resources with a particular tag.""" 3 | 4 | from intercom import utils 5 | from intercom.collection_proxy import CollectionProxy 6 | 7 | 8 | class Tags(object): 9 | """A mixin that provides `by_tag` functionality a resource.""" 10 | 11 | def by_tag(self, _id): 12 | """Return a CollectionProxy to all the tagged resources.""" 13 | collection = utils.resource_class_to_collection_name( 14 | self.collection_class) 15 | finder_url = "/%s?tag_id=%s" % (collection, _id) 16 | return CollectionProxy( 17 | self.client, self.collection_class, collection, finder_url) 18 | -------------------------------------------------------------------------------- /intercom/extended_api_operations/users.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Operation to return all users for a particular Company.""" 3 | 4 | from intercom import utils, user 5 | from intercom.collection_proxy import CollectionProxy 6 | 7 | 8 | class Users(object): 9 | """A mixin that provides `users` functionality to Company.""" 10 | 11 | def users(self, id): 12 | """Return a CollectionProxy to all the users for the specified Company.""" 13 | collection = utils.resource_class_to_collection_name( 14 | self.collection_class) 15 | finder_url = "/%s/%s/users" % (collection, id) 16 | return CollectionProxy( 17 | self.client, user.User, "users", finder_url) 18 | -------------------------------------------------------------------------------- /intercom/job.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # noqa 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Job(Resource): 7 | """A Bulk API Job. 8 | 9 | Ref: https://developers.intercom.io/reference#bulk-job-model 10 | """ 11 | -------------------------------------------------------------------------------- /intercom/lead.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Lead(Resource): 7 | 8 | update_verb = 'put' 9 | identity_vars = ['email', 'user_id'] 10 | collection_name = 'contacts' 11 | 12 | @property 13 | def flat_store_attributes(self): 14 | return ['custom_attributes'] 15 | -------------------------------------------------------------------------------- /intercom/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /intercom/lib/flat_store.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import numbers 4 | import six 5 | 6 | 7 | class FlatStore(dict): 8 | 9 | def __init__(self, *args, **kwargs): 10 | self.update(*args, **kwargs) 11 | 12 | def __setitem__(self, key, value): 13 | if not ( 14 | isinstance(value, numbers.Real) or 15 | isinstance(value, six.string_types) or 16 | value is None 17 | ): 18 | raise ValueError( 19 | "custom data only allows None, string and real number values") 20 | if not isinstance(key, six.string_types): 21 | raise ValueError("custom data only allows string keys") 22 | super(FlatStore, self).__setitem__(key, value) 23 | 24 | def update(self, *args, **kwargs): 25 | if args: 26 | if len(args) > 1: 27 | raise TypeError("update expected at most 1 arguments, " 28 | "got %d" % len(args)) 29 | other = dict(args[0]) 30 | for key in other: 31 | self[key] = other[key] 32 | for key in kwargs: 33 | self[key] = kwargs[key] 34 | 35 | def setdefault(self, key, value=None): 36 | if key not in self: 37 | self[key] = value 38 | return self[key] 39 | -------------------------------------------------------------------------------- /intercom/lib/setter_property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class SetterProperty(object): 4 | 5 | def __init__(self, func, doc=None): 6 | self.func = func 7 | self.__doc__ = doc if doc is not None else func.__doc__ 8 | 9 | def __set__(self, obj, value): 10 | return self.func(obj, value) 11 | -------------------------------------------------------------------------------- /intercom/lib/typed_json_deserializer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import utils 4 | 5 | 6 | class JsonDeserializer(object): 7 | 8 | def __init__(self, json): 9 | self._json = json 10 | self._object_type = None 11 | 12 | @property 13 | def _get_object_type(self): 14 | if self._object_type is None: 15 | self._object_type = self._json.get('type', None) 16 | if self._object_type is None: 17 | raise Exception( 18 | 'No type field found to faciliate deserialization') 19 | return self._object_type 20 | 21 | @property 22 | def _is_list_type(self): 23 | return self._get_object_type.endswith('.list') 24 | 25 | @property 26 | def _object_entity_key(self): 27 | return utils.entity_key_from_type(self._get_object_type) 28 | 29 | def deserialize(self): 30 | if self._is_list_type: 31 | return self.deserialize_collection( 32 | self._json[self._object_entity_key]) 33 | else: 34 | return self.deserialize_object(self._json) 35 | 36 | def deserialize_collection(self, collection_json): 37 | if collection_json is None: 38 | return [] 39 | return [JsonDeserializer(object_json).deserialize() 40 | for object_json in collection_json] 41 | 42 | def deserialize_object(self, object_json): 43 | entity_class = utils.constantize_singular_resource_name( 44 | self._object_entity_key) 45 | return entity_class.from_api(object_json) 46 | -------------------------------------------------------------------------------- /intercom/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Message(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/note.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Note(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/notification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Notification(Resource): 7 | 8 | @property 9 | def model(self): 10 | return self.data.item 11 | 12 | @property 13 | def model_type(self): 14 | return self.model.__class__ 15 | 16 | @property 17 | def load(self): 18 | return self.model.load 19 | -------------------------------------------------------------------------------- /intercom/request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import errors 4 | from datetime import datetime 5 | from pytz import utc 6 | 7 | import certifi 8 | import json 9 | import logging 10 | import os 11 | import requests 12 | 13 | logger = logging.getLogger('intercom.request') 14 | 15 | 16 | def configure_timeout(): 17 | """Configure the request timeout.""" 18 | timeout = os.getenv('INTERCOM_REQUEST_TIMEOUT', '90') 19 | try: 20 | return int(timeout) 21 | except ValueError: 22 | logger.warning('%s is not a valid timeout value.', timeout) 23 | return 90 24 | 25 | 26 | class Request(object): 27 | 28 | timeout = configure_timeout() 29 | 30 | def __init__(self, http_method, path, http_session=None): 31 | self.http_method = http_method 32 | self.path = path 33 | self.http_session = http_session 34 | 35 | def execute(self, base_url, auth, params): 36 | return self.send_request_to_path(base_url, auth, params) 37 | 38 | def send_request_to_path(self, base_url, auth, params=None): 39 | """ Construct an API request, send it to the API, and parse the 40 | response. """ 41 | from intercom import __version__ 42 | req_params = {} 43 | 44 | # full URL 45 | url = base_url + self.path 46 | 47 | headers = { 48 | 'User-Agent': 'python-intercom/' + __version__, 49 | 'AcceptEncoding': 'gzip, deflate', 50 | 'Accept': 'application/json' 51 | } 52 | if self.http_method in ('POST', 'PUT', 'DELETE'): 53 | headers['content-type'] = 'application/json' 54 | req_params['data'] = json.dumps(params, cls=ResourceEncoder) 55 | elif self.http_method == 'GET': 56 | req_params['params'] = params 57 | req_params['headers'] = headers 58 | 59 | # request logging 60 | if logger.isEnabledFor(logging.DEBUG): 61 | logger.debug("Sending %s request to: %s", self.http_method, url) 62 | logger.debug(" headers: %s", headers) 63 | if self.http_method == 'GET': 64 | logger.debug(" params: %s", req_params['params']) 65 | else: 66 | logger.debug(" params: %s", req_params['data']) 67 | 68 | if self.http_session is None: 69 | resp = requests.request( 70 | self.http_method, url, timeout=self.timeout, 71 | auth=auth, verify=certifi.where(), **req_params) 72 | else: 73 | resp = self.http_session.request( 74 | self.http_method, url, timeout=self.timeout, 75 | auth=auth, verify=certifi.where(), **req_params) 76 | 77 | # response logging 78 | if logger.isEnabledFor(logging.DEBUG): 79 | logger.debug("Response received from %s", url) 80 | logger.debug(" encoding=%s status:%s", 81 | resp.encoding, resp.status_code) 82 | logger.debug(" content:\n%s", resp.content) 83 | 84 | parsed_body = self.parse_body(resp) 85 | self.raise_errors_on_failure(resp) 86 | self.set_rate_limit_details(resp) 87 | return parsed_body 88 | 89 | def parse_body(self, resp): 90 | if resp.content and resp.content.strip(): 91 | try: 92 | # use supplied or inferred encoding to decode the 93 | # response content 94 | decoded_body = resp.content.decode( 95 | resp.encoding or resp.apparent_encoding) 96 | body = json.loads(decoded_body) 97 | if body.get('type') == 'error.list': 98 | self.raise_application_errors_on_failure(body, resp.status_code) # noqa 99 | return body 100 | except ValueError: 101 | self.raise_errors_on_failure(resp) 102 | 103 | def set_rate_limit_details(self, resp): 104 | rate_limit_details = {} 105 | headers = resp.headers 106 | limit = headers.get('x-ratelimit-limit', None) 107 | remaining = headers.get('x-ratelimit-remaining', None) 108 | reset = headers.get('x-ratelimit-reset', None) 109 | if limit: 110 | rate_limit_details['limit'] = int(limit) 111 | if remaining: 112 | rate_limit_details['remaining'] = int(remaining) 113 | if reset: 114 | reset_at = datetime.utcfromtimestamp(int(reset)).replace(tzinfo=utc) 115 | rate_limit_details['reset_at'] = reset_at 116 | self.rate_limit_details = rate_limit_details 117 | 118 | def raise_errors_on_failure(self, resp): 119 | if resp.status_code == 404: 120 | raise errors.ResourceNotFound('Resource Not Found') 121 | elif resp.status_code == 401: 122 | raise errors.AuthenticationError('Unauthorized') 123 | elif resp.status_code == 403: 124 | raise errors.AuthenticationError('Forbidden') 125 | elif resp.status_code == 500: 126 | raise errors.ServerError('Server Error') 127 | elif resp.status_code == 502: 128 | raise errors.BadGatewayError('Bad Gateway Error') 129 | elif resp.status_code == 503: 130 | raise errors.ServiceUnavailableError('Service Unavailable') 131 | 132 | def raise_application_errors_on_failure(self, error_list_details, http_code): # noqa 133 | # Currently, we don't support multiple errors 134 | error_details = error_list_details['errors'][0] 135 | error_code = error_details.get('type') 136 | if error_code is None: 137 | error_code = error_details.get('code') 138 | error_context = { 139 | 'http_code': http_code, 140 | 'application_error_code': error_code 141 | } 142 | error_class = errors.error_codes.get(error_code) 143 | if error_class is None: 144 | # unexpected error 145 | if error_code: 146 | message = self.message_for_unexpected_error_with_type( 147 | error_details, http_code) 148 | else: 149 | message = self.message_for_unexpected_error_without_type( 150 | error_details, http_code) 151 | error_class = errors.UnexpectedError 152 | else: 153 | message = error_details.get('message') 154 | raise error_class(message, error_context) 155 | 156 | def message_for_unexpected_error_with_type(self, error_details, http_code): # noqa 157 | error_type = error_details.get('type') 158 | message = error_details.get('message') 159 | 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 160 | 161 | def message_for_unexpected_error_without_type(self, error_details, http_code): # noqa 162 | message = error_details['message'] 163 | 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 164 | 165 | 166 | class ResourceEncoder(json.JSONEncoder): 167 | def default(self, o): 168 | if hasattr(o, 'attributes'): 169 | # handle API resources 170 | return o.attributes 171 | return super(ResourceEncoder, self).default(o) 172 | -------------------------------------------------------------------------------- /intercom/scroll_collection_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Proxy for the Scroll API.""" 3 | import six 4 | from intercom import HttpError 5 | 6 | 7 | class ScrollCollectionProxy(six.Iterator): 8 | """A proxy to iterate over resources returned by the Scroll API.""" 9 | 10 | def __init__(self, client, resource_class, resource_name, scroll_url): 11 | """Initialise the proxy.""" 12 | self.client = client 13 | 14 | # resource name 15 | self.resource_name = resource_name 16 | 17 | # resource class 18 | self.resource_class = resource_class 19 | 20 | # the original URL to retrieve the resources 21 | self.scroll_url = scroll_url 22 | 23 | # the identity of the scroll, extracted from the response 24 | self.scroll_param = None 25 | 26 | # an iterator over the resources found in the response 27 | self.resources = None 28 | 29 | # a link to the next page of results 30 | self.next_page = None 31 | 32 | def __iter__(self): 33 | """Return self as the proxy has __next__ implemented.""" 34 | return self 35 | 36 | def __next__(self): 37 | """Return the next resource from the response.""" 38 | if self.resources is None: 39 | # get the first page of results 40 | self.get_first_page() 41 | 42 | # try to get a resource if there are no more in the 43 | # current resource iterator (StopIteration is raised) 44 | # try to get the next page of results first 45 | try: 46 | resource = six.next(self.resources) 47 | except StopIteration: 48 | self.get_next_page() 49 | resource = six.next(self.resources) 50 | 51 | instance = self.resource_class(**resource) 52 | return instance 53 | 54 | def __getitem__(self, index): 55 | """Return an exact item from the proxy.""" 56 | for i in range(index): 57 | six.next(self) 58 | return six.next(self) 59 | 60 | def get_first_page(self): 61 | """Return the first page of results.""" 62 | return self.get_page(self.scroll_param) 63 | 64 | def get_next_page(self): 65 | """Return the next page of results.""" 66 | return self.get_page(self.scroll_param) 67 | 68 | def get_page(self, scroll_param=None): 69 | """Retrieve a page of results from the Scroll API.""" 70 | if scroll_param is None: 71 | response = self.client.get(self.scroll_url, {}) 72 | else: 73 | response = self.client.get(self.scroll_url, {'scroll_param': scroll_param}) 74 | 75 | if response is None: 76 | raise HttpError('Http Error - No response entity returned') 77 | 78 | # create the resource iterator 79 | collection = response[self.resource_name] 80 | self.resources = iter(collection) 81 | # grab the next page URL if one exists 82 | self.scroll_param = self.extract_scroll_param(response) 83 | 84 | def records_present(self, response): 85 | """Return whether there are resources in the response.""" 86 | return len(response.get(self.resource_name)) > 0 87 | 88 | def extract_scroll_param(self, response): 89 | """Extract the scroll_param from the response.""" 90 | if self.records_present(response): 91 | return response.get('scroll_param') 92 | -------------------------------------------------------------------------------- /intercom/segment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Segment(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/service/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /intercom/service/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import admin 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.find import Find 6 | from intercom.service.base_service import BaseService 7 | 8 | 9 | class Admin(BaseService, All, Find): 10 | 11 | @property 12 | def collection_class(self): 13 | return admin.Admin 14 | -------------------------------------------------------------------------------- /intercom/service/base_service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class BaseService(object): 5 | 6 | def __init__(self, client): 7 | self.client = client 8 | 9 | @property 10 | def collection_class(self): 11 | raise NotImplementedError 12 | 13 | def from_api(self, api_response): 14 | obj = self.collection_class() 15 | obj.from_response(api_response) 16 | return obj 17 | -------------------------------------------------------------------------------- /intercom/service/company.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import company 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.delete import Delete 6 | from intercom.api_operations.find import Find 7 | from intercom.api_operations.find_all import FindAll 8 | from intercom.api_operations.save import Save 9 | from intercom.api_operations.load import Load 10 | from intercom.extended_api_operations.users import Users 11 | from intercom.extended_api_operations.tags import Tags 12 | from intercom.service.base_service import BaseService 13 | 14 | 15 | class Company(BaseService, All, Delete, Find, FindAll, Save, Load, Users, Tags): 16 | 17 | @property 18 | def collection_class(self): 19 | return company.Company 20 | 21 | # require 'intercom/extended_api_operations/segments' 22 | -------------------------------------------------------------------------------- /intercom/service/conversation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Service module for Conversations.""" 3 | 4 | from intercom import conversation 5 | from intercom import utils 6 | from intercom.api_operations.find import Find 7 | from intercom.api_operations.find_all import FindAll 8 | from intercom.api_operations.load import Load 9 | from intercom.api_operations.save import Save 10 | from intercom.service.base_service import BaseService 11 | 12 | 13 | class Conversation(BaseService, Find, FindAll, Save, Load): 14 | """Service class for Conversations.""" 15 | 16 | @property 17 | def collection(self): 18 | """Return the name of the collection.""" 19 | return utils.resource_class_to_collection_name(self.collection_class) 20 | 21 | @property 22 | def collection_class(self): 23 | """Return the class of the collection.""" 24 | return conversation.Conversation 25 | 26 | def resource_url(self, _id): 27 | """Return the URL for the specified resource in this collection.""" 28 | return "/%s/%s/reply" % (self.collection, _id) 29 | 30 | def reply(self, **reply_data): 31 | """Reply to a message.""" 32 | return self.__reply(reply_data) 33 | 34 | def assign(self, **reply_data): 35 | """Assign a conversation to a user.""" 36 | reply_data['type'] = 'admin' 37 | reply_data['message_type'] = 'assignment' 38 | return self.__reply(reply_data) 39 | 40 | def open(self, **reply_data): 41 | """Mark a conversation as open.""" 42 | reply_data['type'] = 'admin' 43 | reply_data['message_type'] = 'open' 44 | return self.__reply(reply_data) 45 | 46 | def close(self, **reply_data): 47 | """Mark a conversation as closed.""" 48 | reply_data['type'] = 'admin' 49 | reply_data['message_type'] = 'close' 50 | return self.__reply(reply_data) 51 | 52 | def mark_read(self, _id): 53 | """Mark a conversation as read.""" 54 | data = {'read': True} 55 | response = self.client.put(self.resource_url(_id), data) 56 | return self.collection_class().from_response(response) 57 | 58 | def __reply(self, reply_data): 59 | """Send requests to the resource handler.""" 60 | _id = reply_data.pop('id') 61 | reply_data['conversation_id'] = _id 62 | response = self.client.post(self.resource_url(_id), reply_data) 63 | return self.collection_class().from_response(response) 64 | -------------------------------------------------------------------------------- /intercom/service/count.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import count 4 | from intercom.api_operations.find import Find 5 | from intercom.service.base_service import BaseService 6 | 7 | 8 | class Count(BaseService, Find): 9 | 10 | @property 11 | def collection_class(self): 12 | return count.Count 13 | 14 | def for_app(self): 15 | return self.find() 16 | 17 | def for_type(self, type, count=None): 18 | return self.find(type=type, count=count) 19 | -------------------------------------------------------------------------------- /intercom/service/event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import event 4 | from intercom.api_operations.bulk import Submit 5 | from intercom.api_operations.save import Save 6 | from intercom.api_operations.find_all import FindAll 7 | from intercom.service.base_service import BaseService 8 | from intercom.collection_proxy import CollectionProxy 9 | 10 | 11 | class EventCollectionProxy(CollectionProxy): 12 | 13 | def paging_info_present(self, response): 14 | return 'pages' in response and 'next' in response['pages'] 15 | 16 | 17 | class Event(BaseService, Save, Submit, FindAll): 18 | 19 | proxy_class = EventCollectionProxy 20 | 21 | @property 22 | def collection_class(self): 23 | return event.Event 24 | -------------------------------------------------------------------------------- /intercom/service/job.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import job 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.bulk import LoadErrorFeed 6 | from intercom.api_operations.find import Find 7 | from intercom.api_operations.find_all import FindAll 8 | from intercom.api_operations.save import Save 9 | from intercom.api_operations.load import Load 10 | from intercom.service.base_service import BaseService 11 | 12 | 13 | class Job(BaseService, All, Find, FindAll, Save, Load, LoadErrorFeed): 14 | 15 | @property 16 | def collection_class(self): 17 | return job.Job 18 | -------------------------------------------------------------------------------- /intercom/service/lead.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # noqa 2 | 3 | from intercom import lead 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.convert import Convert 6 | from intercom.api_operations.find import Find 7 | from intercom.api_operations.find_all import FindAll 8 | from intercom.api_operations.delete import Delete 9 | from intercom.api_operations.save import Save 10 | from intercom.api_operations.load import Load 11 | from intercom.service.base_service import BaseService 12 | 13 | 14 | class Lead(BaseService, All, Find, FindAll, Delete, Save, Load, Convert): 15 | """Leads are useful for representing logged-out users of your application. 16 | 17 | Ref: https://developers.intercom.io/reference#leads 18 | """ 19 | 20 | @property 21 | def collection_class(self): 22 | """The collection class that represents this resource.""" 23 | return lead.Lead 24 | -------------------------------------------------------------------------------- /intercom/service/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import message 4 | from intercom.api_operations.save import Save 5 | from intercom.service.base_service import BaseService 6 | 7 | 8 | class Message(BaseService, Save): 9 | 10 | @property 11 | def collection_class(self): 12 | return message.Message 13 | -------------------------------------------------------------------------------- /intercom/service/note.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import note 4 | from intercom.api_operations.find import Find 5 | from intercom.api_operations.find_all import FindAll 6 | from intercom.api_operations.save import Save 7 | from intercom.api_operations.load import Load 8 | from intercom.service.base_service import BaseService 9 | 10 | 11 | class Note(BaseService, Find, FindAll, Save, Load): 12 | 13 | @property 14 | def collection_class(self): 15 | return note.Note 16 | -------------------------------------------------------------------------------- /intercom/service/segment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import segment 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.find import Find 6 | from intercom.service.base_service import BaseService 7 | 8 | 9 | class Segment(BaseService, All, Find): 10 | 11 | @property 12 | def collection_class(self): 13 | return segment.Segment 14 | -------------------------------------------------------------------------------- /intercom/service/subscription.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import subscription 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.find import Find 6 | from intercom.api_operations.find_all import FindAll 7 | from intercom.api_operations.save import Save 8 | from intercom.api_operations.delete import Delete 9 | from intercom.service.base_service import BaseService 10 | 11 | 12 | class Subscription(BaseService, All, Find, FindAll, Save, Delete): 13 | 14 | @property 15 | def collection_class(self): 16 | return subscription.Subscription 17 | -------------------------------------------------------------------------------- /intercom/service/tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import tag 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.find import Find 6 | from intercom.api_operations.find_all import FindAll 7 | from intercom.api_operations.save import Save 8 | from intercom.service.base_service import BaseService 9 | 10 | 11 | class Tag(BaseService, All, Find, FindAll, Save): 12 | 13 | @property 14 | def collection_class(self): 15 | return tag.Tag 16 | 17 | def tag(self, **params): 18 | params['tag_or_untag'] = 'tag' 19 | return self.create(**params) 20 | 21 | def untag(self, **params): 22 | params['tag_or_untag'] = 'untag' 23 | for user_or_company in self._users_or_companies(params): 24 | user_or_company['untag'] = True 25 | return self.create(**params) 26 | 27 | def _users_or_companies(self, params): 28 | if 'users' in params: 29 | return params['users'] 30 | if 'companies' in params: 31 | return params['companies'] 32 | return [] 33 | -------------------------------------------------------------------------------- /intercom/service/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom import user 4 | from intercom.api_operations.all import All 5 | from intercom.api_operations.bulk import Submit 6 | from intercom.api_operations.find import Find 7 | from intercom.api_operations.find_all import FindAll 8 | from intercom.api_operations.delete import Delete 9 | from intercom.api_operations.save import Save 10 | from intercom.api_operations.load import Load 11 | from intercom.api_operations.scroll import Scroll 12 | from intercom.extended_api_operations.tags import Tags 13 | from intercom.service.base_service import BaseService 14 | 15 | 16 | class User(BaseService, All, Find, FindAll, Delete, Save, Load, Submit, Tags, Scroll): 17 | 18 | @property 19 | def collection_class(self): 20 | return user.User 21 | -------------------------------------------------------------------------------- /intercom/subscription.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Subscription(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | 5 | 6 | class Tag(Resource): 7 | pass 8 | -------------------------------------------------------------------------------- /intercom/traits/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /intercom/traits/api_resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import calendar 4 | import datetime 5 | import time 6 | 7 | from intercom.lib.flat_store import FlatStore 8 | from intercom.lib.typed_json_deserializer import JsonDeserializer 9 | from pytz import utc 10 | 11 | 12 | def type_field(attribute): 13 | return attribute == "type" 14 | 15 | 16 | def timestamp_field(attribute): 17 | return attribute.endswith('_at') 18 | 19 | 20 | def custom_attribute_field(attribute): 21 | return attribute == 'custom_attributes' 22 | 23 | 24 | def typed_value(value): 25 | return hasattr(value, 'keys') and 'type' in value 26 | 27 | 28 | def datetime_value(value): 29 | return hasattr(value, "timetuple") 30 | 31 | 32 | def to_datetime_value(value): 33 | if value: 34 | return datetime.datetime.utcfromtimestamp(int(value)).replace(tzinfo=utc) 35 | 36 | 37 | class Resource(object): 38 | client = None 39 | changed_attributes = [] 40 | 41 | def __init__(_self, *args, **params): # noqa 42 | if args: 43 | _self.client = args[0] 44 | 45 | # intercom includes a 'self' field in the JSON, to avoid the naming 46 | # conflict we go with _self here 47 | _self.from_dict(params) 48 | 49 | if hasattr(_self, 'flat_store_attributes'): 50 | for attr in _self.flat_store_attributes: 51 | if not hasattr(_self, attr): 52 | setattr(_self, attr, FlatStore()) 53 | 54 | def _flat_store_attribute(self, attribute): 55 | if hasattr(self, 'flat_store_attributes'): 56 | return attribute in self.flat_store_attributes 57 | return False 58 | 59 | @classmethod 60 | def from_api(cls, response): 61 | obj = cls() 62 | obj.from_response(response) 63 | return obj 64 | 65 | def from_response(self, response): 66 | self.from_dict(response) 67 | return self 68 | 69 | def from_dict(self, dict): 70 | for attribute, value in list(dict.items()): 71 | if type_field(attribute): 72 | continue 73 | setattr(self, attribute, value) 74 | if hasattr(self, 'id'): 75 | # already exists in Intercom 76 | self.changed_attributes = [] 77 | return self 78 | 79 | def to_dict(self): 80 | a_dict = {} 81 | for name in list(self.__dict__.keys()): 82 | if name == "changed_attributes": 83 | continue 84 | a_dict[name] = self.__dict__[name] # direct access 85 | return a_dict 86 | 87 | @property 88 | def attributes(self): 89 | res = {} 90 | for name, value in list(self.__dict__.items()): 91 | if self.submittable_attribute(name, value): 92 | res[name] = value 93 | return res 94 | 95 | def submittable_attribute(self, name, value): 96 | return name in self.changed_attributes or (isinstance(value, FlatStore) and name in self.flat_store_attributes) # noqa 97 | 98 | def __getattribute__(self, attribute): 99 | value = super(Resource, self).__getattribute__(attribute) 100 | if timestamp_field(attribute): 101 | return to_datetime_value(value) 102 | else: 103 | return value 104 | 105 | def __setattr__(self, attribute, value): 106 | if typed_value(value) and not custom_attribute_field(attribute): 107 | value_to_set = JsonDeserializer(value).deserialize() 108 | elif self._flat_store_attribute(attribute): 109 | value_to_set = FlatStore(value) 110 | elif timestamp_field(attribute) and datetime_value(value): 111 | value_to_set = calendar.timegm(value.utctimetuple()) 112 | else: 113 | value_to_set = value 114 | if attribute != 'changed_attributes': 115 | self.changed_attributes.append(attribute) 116 | super(Resource, self).__setattr__(attribute, value_to_set) 117 | -------------------------------------------------------------------------------- /intercom/traits/incrementable_attributes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class IncrementableAttributes(object): 5 | 6 | def increment(self, key, value=1): 7 | existing_value = self.custom_attributes.get(key, 0) 8 | if existing_value is None: 9 | existing_value = 0 10 | self.custom_attributes[key] = existing_value + value 11 | -------------------------------------------------------------------------------- /intercom/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from intercom.traits.api_resource import Resource 4 | from intercom.traits.incrementable_attributes import IncrementableAttributes 5 | 6 | 7 | class User(Resource, IncrementableAttributes): 8 | 9 | update_verb = 'post' 10 | identity_vars = ['id', 'email', 'user_id'] 11 | 12 | @property 13 | def flat_store_attributes(self): 14 | return ['custom_attributes'] 15 | -------------------------------------------------------------------------------- /intercom/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import inflection 4 | import six 5 | 6 | 7 | def pluralize(str): 8 | return inflection.pluralize(str) 9 | 10 | 11 | def entity_key_from_type(type): 12 | if '.' in type: 13 | is_list = type.split('.')[1] == 'list' 14 | entity_name = type.split('.')[0] 15 | if is_list: 16 | return pluralize(entity_name) 17 | else: 18 | entity_name = type 19 | return entity_name 20 | 21 | 22 | def constantize_singular_resource_name(resource_name): 23 | class_name = inflection.camelize(resource_name) 24 | return define_lightweight_class(resource_name, class_name) 25 | 26 | 27 | def resource_class_to_collection_name(cls): 28 | if hasattr(cls, 'collection_name'): 29 | return cls.collection_name 30 | return pluralize(cls.__name__.lower()) 31 | 32 | 33 | def resource_class_to_name(cls): 34 | return cls.__name__.lower() 35 | 36 | 37 | CLASS_REGISTRY = {} 38 | 39 | 40 | def define_lightweight_class(resource_name, class_name): 41 | """Return a lightweight class for deserialized payload objects.""" 42 | from intercom.api_operations.load import Load 43 | from intercom.traits.api_resource import Resource 44 | 45 | if class_name in CLASS_REGISTRY: 46 | return CLASS_REGISTRY[class_name] 47 | 48 | class Meta(type): 49 | def __new__(cls, name, bases, attributes): 50 | return super(Meta, cls).__new__( 51 | cls, str(class_name), bases, attributes) 52 | 53 | @six.add_metaclass(Meta) 54 | class DynamicClass(Resource, Load): 55 | resource_type = resource_name 56 | 57 | dyncls = DynamicClass 58 | CLASS_REGISTRY[class_name] = dyncls 59 | return dyncls 60 | -------------------------------------------------------------------------------- /pylint.conf: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Profiled execution. 11 | profile=no 12 | 13 | # Add files or directories to the blocked list. They should be base names, not 14 | # paths. 15 | ignore=CVS 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=yes 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | 25 | [MESSAGES CONTROL] 26 | 27 | # Enable the message, report, category or checker with the given id(s). You can 28 | # either give multiple identifier separated by comma (,) or put this option 29 | # multiple time. 30 | #enable= 31 | 32 | # Disable the message, report, category or checker with the given id(s). You 33 | # can either give multiple identifier separated by comma (,) or put this option 34 | # multiple time (only on the command line, not in the configuration file where 35 | # it should appear only once). 36 | disable=W0142 37 | 38 | 39 | [REPORTS] 40 | 41 | # Set the output format. Available formats are text, parseable, colorized, msvs 42 | # (visual studio) and html 43 | output-format=text 44 | 45 | # Include message's id in output 46 | include-ids=no 47 | 48 | # Put messages in a separate file for each module / package specified on the 49 | # command line instead of printing them on stdout. Reports (if any) will be 50 | # written in a file name "pylint_global.[txt|html]". 51 | files-output=no 52 | 53 | # Tells whether to display a full report or only the messages 54 | reports=yes 55 | 56 | # Python expression which should return a note less than 10 (10 is the highest 57 | # note). You have access to the variables errors warning, statement which 58 | # respectively contain the number of errors / warnings messages and the total 59 | # number of statements analyzed. This is used by the global evaluation report 60 | # (RP0004). 61 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 62 | 63 | # Add a comment according to your evaluation note. This is used by the global 64 | # evaluation report (RP0004). 65 | comment=no 66 | 67 | 68 | [BASIC] 69 | 70 | # Required attributes for module, separated by a comma 71 | required-attributes= 72 | 73 | # List of builtins function names that should not be used, separated by a comma 74 | bad-functions=map,filter,apply,input 75 | 76 | # Regular expression which should only match correct module names 77 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 78 | 79 | # Regular expression which should only match correct module level names 80 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 81 | 82 | # Regular expression which should only match correct class names 83 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 84 | 85 | # Regular expression which should only match correct function names 86 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 87 | 88 | # Regular expression which should only match correct method names 89 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 90 | 91 | # Regular expression which should only match correct instance attribute names 92 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 93 | 94 | # Regular expression which should only match correct argument names 95 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 96 | 97 | # Regular expression which should only match correct variable names 98 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 99 | 100 | # Regular expression which should only match correct list comprehension / 101 | # generator expression variable names 102 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 103 | 104 | # Good variable names which should always be accepted, separated by a comma 105 | good-names=i,j,k,ex,Run,_ 106 | 107 | # Bad variable names which should always be refused, separated by a comma 108 | bad-names=foo,bar,baz,toto,tutu,tata 109 | 110 | # Regular expression which should only match functions or classes name which do 111 | # not require a docstring 112 | no-docstring-rgx=__.*__ 113 | 114 | 115 | [FORMAT] 116 | 117 | # Maximum number of characters on a single line. 118 | max-line-length=100 119 | 120 | # Maximum number of lines in a module 121 | max-module-lines=1000 122 | 123 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 124 | # tab). 125 | indent-string=' ' 126 | 127 | 128 | [MISCELLANEOUS] 129 | 130 | # List of note tags to take in consideration, separated by a comma. 131 | notes=FIXME,XXX,TODO 132 | 133 | 134 | [SIMILARITIES] 135 | 136 | # Minimum lines number of a similarity. 137 | min-similarity-lines=4 138 | 139 | # Ignore comments when computing similarities. 140 | ignore-comments=yes 141 | 142 | # Ignore docstrings when computing similarities. 143 | ignore-docstrings=yes 144 | 145 | 146 | [TYPECHECK] 147 | 148 | # Tells whether missing members accessed in mixin class should be ignored. A 149 | # mixin class is detected if its name ends with "mixin" (case insensitive). 150 | ignore-mixin-members=yes 151 | 152 | # List of classes names for which member attributes should not be checked 153 | # (useful for classes with attributes dynamically set). 154 | ignored-classes=SQLObject 155 | 156 | # When zope mode is activated, add a predefined set of Zope acquired attributes 157 | # to generated-members. 158 | zope=no 159 | 160 | # List of members which are set dynamically and missed by pylint inference 161 | # system, and so shouldn't trigger E0201 when accessed. Python regular 162 | # expressions are accepted. 163 | generated-members=REQUEST,acl_users,aq_parent 164 | 165 | 166 | [VARIABLES] 167 | 168 | # Tells whether we should check for unused import in __init__ files. 169 | init-import=no 170 | 171 | # A regular expression matching the beginning of the name of dummy variables 172 | # (i.e. not used). 173 | dummy-variables-rgx=_|dummy 174 | 175 | # List of additional names supposed to be defined in builtins. Remember that 176 | # you should avoid to define new builtins when possible. 177 | additional-builtins= 178 | 179 | 180 | [CLASSES] 181 | 182 | # List of interface methods to ignore, separated by a comma. This is used for 183 | # instance to not check methods defines in Zope's Interface base class. 184 | ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by 185 | 186 | # List of method names used to declare (i.e. assign) instance attributes. 187 | defining-attr-methods=__init__,__new__,setUp 188 | 189 | # List of valid names for the first argument in a class method. 190 | valid-classmethod-first-arg=cls 191 | 192 | 193 | [DESIGN] 194 | 195 | # Maximum number of arguments for function / method 196 | max-args=10 197 | 198 | # Argument names that match this expression will be ignored. Default to name 199 | # with leading underscore 200 | ignored-argument-names=_.* 201 | 202 | # Maximum number of locals for function / method body 203 | max-locals=15 204 | 205 | # Maximum number of return / yield for function / method body 206 | max-returns=6 207 | 208 | # Maximum number of branch for function / method body 209 | max-branchs=12 210 | 211 | # Maximum number of statements in function / method body 212 | max-statements=50 213 | 214 | # Maximum number of parents for a class (see R0901). 215 | max-parents=7 216 | 217 | # Maximum number of attributes for a class (see R0902). 218 | max-attributes=7 219 | 220 | # Minimum number of public methods for a class (see R0903). 221 | min-public-methods=2 222 | 223 | # Maximum number of public methods for a class (see R0904). 224 | max-public-methods=30 225 | 226 | 227 | [IMPORTS] 228 | 229 | # Deprecated modules which should not be used, separated by a comma 230 | deprecated-modules=regsub,string,TERMIOS,Bastion,rexec 231 | 232 | # Create a graph of every (i.e. internal and external) dependencies in the 233 | # given file (report RP0402 must not be disabled) 234 | import-graph= 235 | 236 | # Create a graph of external dependencies in the given file (report RP0402 must 237 | # not be disabled) 238 | ext-import-graph= 239 | 240 | # Create a graph of internal dependencies in the given file (report RP0402 must 241 | # not be disabled) 242 | int-import-graph= 243 | 244 | 245 | [EXCEPTIONS] 246 | 247 | # Exceptions that will emit a warning when being caught. Defaults to 248 | # "Exception" 249 | overgeneral-exceptions=Exception 250 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # Runtime dependencies. 3 | # 4 | certifi 5 | inflection==0.3.0 6 | pytz==2016.7 7 | requests==2.32.2 8 | urllib3==1.26.19 9 | six==1.9.0 10 | -------------------------------------------------------------------------------- /rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | certifi 2 | inflection==0.3.0 3 | requests==2.32.2 4 | urllib3==1.26.19 5 | six==1.9.0 6 | sphinx-rtd-theme==0.1.7 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2012 John Keyes 3 | # 4 | # http://jkeyes.mit-license.org/ 5 | # 6 | 7 | import os 8 | import re 9 | 10 | from setuptools import find_packages 11 | from setuptools import setup 12 | 13 | with open(os.path.join('intercom', '__init__.py')) as init: 14 | source = init.read() 15 | m = re.search("__version__ = '(.*)'", source, re.M) 16 | __version__ = m.groups()[0] 17 | 18 | with open('README.rst') as readme: 19 | long_description = readme.read() 20 | 21 | setup( 22 | name="python-intercom", 23 | version=__version__, 24 | description="Intercom API wrapper", 25 | long_description=long_description, 26 | author="John Keyes", 27 | author_email="john@keyes.ie", 28 | license="MIT License", 29 | url="http://github.com/jkeyes/python-intercom", 30 | keywords='Intercom crm python', 31 | classifiers=[ 32 | 'Programming Language :: Python :: 2', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.4', 36 | ], 37 | packages=find_packages(), 38 | include_package_data=True, 39 | install_requires=["requests", "inflection", "certifi", "six", "pytz"], 40 | zip_safe=False 41 | ) 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Copyright 2012 keyes.ie 4 | # 5 | # License: http://jkeyes.mit-license.org/ 6 | # 7 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | 5 | from datetime import datetime 6 | # from intercom import Company 7 | from intercom import ResourceNotFound 8 | # from intercom import User 9 | 10 | 11 | def get_timestamp(): 12 | now = datetime.utcnow() 13 | return int(time.mktime(now.timetuple())) 14 | 15 | 16 | def get_or_create_user(client, timestamp): 17 | # get user 18 | email = '%s@example.com' % (timestamp) 19 | try: 20 | user = client.users.find(email=email) 21 | except ResourceNotFound: 22 | # Create a user 23 | user = client.users.create( 24 | email=email, 25 | user_id=timestamp, 26 | name="Ada %s" % (timestamp)) 27 | time.sleep(5) 28 | return user 29 | 30 | 31 | def get_or_create_company(client, timestamp): 32 | name = 'Company %s' % (timestamp) 33 | 34 | # get company 35 | try: 36 | company = client.companies.find(name=name) 37 | except ResourceNotFound: 38 | # Create a company 39 | company = client.companies.create( 40 | company_id=timestamp, name=name) 41 | return company 42 | 43 | 44 | def delete_user(client, resource): 45 | try: 46 | client.users.delete(resource) 47 | except ResourceNotFound: 48 | # not much we can do here 49 | pass 50 | 51 | 52 | def delete_company(client, resource): 53 | try: 54 | client.companies.delete(resource) 55 | except ResourceNotFound: 56 | # not much we can do here 57 | pass 58 | -------------------------------------------------------------------------------- /tests/integration/issues/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/integration/issues/test_72.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | import time 6 | 7 | from intercom.client import Client 8 | 9 | intercom = Client( 10 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 11 | 12 | 13 | class Issue72Test(unittest.TestCase): 14 | 15 | def test(self): 16 | intercom.users.create(email='me@example.com') 17 | # no exception here as empty response expected 18 | data = { 19 | 'event_name': 'Eventful 1', 20 | 'created_at': int(time.time()), 21 | 'email': 'me@example.com' 22 | } 23 | intercom.events.create(**data) 24 | -------------------------------------------------------------------------------- /tests/integration/issues/test_73.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | How do I record when a User has started a new session? 4 | """ 5 | 6 | import os 7 | import unittest 8 | 9 | from intercom.client import Client 10 | 11 | intercom = Client( 12 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 13 | 14 | 15 | class Issue73Test(unittest.TestCase): 16 | 17 | def test(self): 18 | user = intercom.users.create(email='bingo@example.com') 19 | # store current session count 20 | session_count = user.session_count 21 | 22 | # register a new session 23 | user.new_session = True 24 | intercom.users.save(user) 25 | 26 | # count has increased by 1 27 | self.assertEquals(session_count + 1, user.session_count) 28 | 29 | # register a new session 30 | user.new_session = True 31 | intercom.users.save(user) 32 | 33 | # count has increased by 1 34 | self.assertEquals(session_count + 2, user.session_count) 35 | -------------------------------------------------------------------------------- /tests/integration/test_admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | 7 | intercom = Client( 8 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 9 | 10 | 11 | class AdminTest(unittest.TestCase): 12 | 13 | def test(self): 14 | # Iterate over all admins 15 | for admin in intercom.admins.all(): 16 | self.assertIsNotNone(admin.id) 17 | self.assertIsNotNone(admin.email) 18 | -------------------------------------------------------------------------------- /tests/integration/test_company.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | from . import delete_company 7 | from . import delete_user 8 | from . import get_or_create_user 9 | from . import get_or_create_company 10 | from . import get_timestamp 11 | 12 | intercom = Client( 13 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 14 | 15 | 16 | class CompanyTest(unittest.TestCase): 17 | 18 | @classmethod 19 | def setup_class(cls): 20 | nowstamp = get_timestamp() 21 | cls.company = get_or_create_company(intercom, nowstamp) 22 | cls.user = get_or_create_user(intercom, nowstamp) 23 | 24 | @classmethod 25 | def teardown_class(cls): 26 | delete_company(intercom, cls.company) 27 | delete_user(intercom, cls.user) 28 | 29 | def test_add_user(self): 30 | user = intercom.users.find(email=self.user.email) 31 | user.companies = [ 32 | {"company_id": 6, "name": "Intercom"}, 33 | {"company_id": 9, "name": "Test Company"} 34 | ] 35 | intercom.users.save(user) 36 | user = intercom.users.find(email=self.user.email) 37 | self.assertEqual(len(user.companies), 2) 38 | self.assertEqual(user.companies[0].company_id, "9") 39 | 40 | def test_add_user_custom_attributes(self): 41 | user = intercom.users.find(email=self.user.email) 42 | user.companies = [ 43 | { 44 | "id": 6, 45 | "name": "Intercom", 46 | "custom_attributes": { 47 | "referral_source": "Google" 48 | } 49 | } 50 | ] 51 | intercom.users.save(user) 52 | user = intercom.users.find(email=self.user.email) 53 | self.assertEqual(len(user.companies), 2) 54 | self.assertEqual(user.companies[0].company_id, "9") 55 | 56 | # check the custom attributes 57 | company = intercom.companies.find(company_id=6) 58 | self.assertEqual( 59 | company.custom_attributes['referral_source'], "Google") 60 | 61 | def test_find_by_company_id(self): 62 | # Find a company by company_id 63 | company = intercom.companies.find(company_id=self.company.company_id) 64 | self.assertEqual(company.company_id, self.company.company_id) 65 | 66 | def test_find_by_company_name(self): 67 | # Find a company by name 68 | company = intercom.companies.find(name=self.company.name) 69 | self.assertEqual(company.name, self.company.name) 70 | 71 | def test_find_by_id(self): 72 | # Find a company by _id 73 | company = intercom.companies.find(id=self.company.id) 74 | self.assertEqual(company.company_id, self.company.company_id) 75 | 76 | def test_update(self): 77 | # Find a company by id 78 | company = intercom.companies.find(id=self.company.id) 79 | # Update a company 80 | now = get_timestamp() 81 | updated_name = 'Company %s' % (now) 82 | company.name = updated_name 83 | intercom.companies.save(company) 84 | company = intercom.companies.find(id=self.company.id) 85 | self.assertEqual(company.name, updated_name) 86 | 87 | def test_iterate(self): 88 | # Iterate over all companies 89 | for company in intercom.companies.all(): 90 | self.assertTrue(company.id is not None) 91 | 92 | def test_users(self): 93 | company = intercom.companies.find(id=self.company.id) 94 | # Get a list of users in a company 95 | for user in intercom.companies.users(company.id): 96 | self.assertIsNotNone(user.email) 97 | -------------------------------------------------------------------------------- /tests/integration/test_conversations.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | # from intercom import Admin 7 | # from intercom import Conversation 8 | # from intercom import Message 9 | from . import delete_user 10 | from . import get_or_create_user 11 | from . import get_timestamp 12 | 13 | intercom = Client( 14 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 15 | 16 | 17 | class ConversationTest(unittest.TestCase): 18 | @classmethod 19 | def setup_class(cls): 20 | # get admin 21 | cls.admin = intercom.admins.all()[1] 22 | 23 | # get user 24 | timestamp = get_timestamp() 25 | cls.user = get_or_create_user(intercom, timestamp) 26 | cls.email = cls.user.email 27 | 28 | # send user message 29 | message_data = { 30 | 'from': { 31 | 'type': "user", 32 | 'id': cls.user.id 33 | }, 34 | 'body': "Hey" 35 | } 36 | cls.user_message = intercom.messages.create(**message_data) 37 | 38 | conversations = intercom.conversations.find_all() 39 | cls.user_init_conv = conversations[0] 40 | # send admin reply 41 | cls.admin_conv = intercom.conversations.reply( 42 | id=cls.user_init_conv.id, 43 | type='admin', admin_id=cls.admin.id, 44 | message_type='comment', body='There') 45 | 46 | @classmethod 47 | def teardown_class(cls): 48 | delete_user(intercom, cls.user) 49 | 50 | def test_find_all_admin(self): 51 | # FINDING CONVERSATIONS FOR AN ADMIN 52 | # Iterate over all conversations (open and closed) assigned to an admin 53 | for convo in intercom.conversations.find_all(type='admin', id=self.admin.id): # noqa 54 | self.assertIsNotNone(convo.id) 55 | self.admin_conv.id = convo.id 56 | 57 | def test_find_all_open_admin(self): 58 | # Iterate over all open conversations assigned to an admin 59 | for convo in intercom.conversations.find_all( 60 | type='admin', id=self.admin.id, open=True): 61 | self.assertIsNotNone(convo.id) 62 | 63 | def test_find_all_closed_admin(self): 64 | # Iterate over closed conversations assigned to an admin 65 | for convo in intercom.conversations.find_all( 66 | type='admin', id=self.admin.id, open=False): 67 | self.assertIsNotNone(convo.id) 68 | 69 | def test_find_all_closed_before_admin(self): 70 | for convo in intercom.conversations.find_all( 71 | type='admin', id=self.admin.id, open=False, 72 | before=1374844930): 73 | self.assertIsNotNone(convo.id) 74 | 75 | def test_find_all_user(self): 76 | # FINDING CONVERSATIONS FOR A USER 77 | # Iterate over all conversations (read + unread, correct) with a 78 | # user based on the users email 79 | for convo in intercom.conversations.find_all(email=self.email, type='user'): # noqa 80 | self.assertIsNotNone(convo.id) 81 | 82 | def test_find_all_read(self): 83 | # Iterate over through all conversations (read + unread) with a 84 | # user based on the users email 85 | for convo in intercom.conversations.find_all( 86 | email=self.email, type='user', unread=False): 87 | self.assertIsNotNone(convo.id) 88 | 89 | def test_find_all_unread(self): 90 | # Iterate over all unread conversations with a user based on the 91 | # users email 92 | for convo in intercom.conversations.find_all( 93 | email=self.email, type='user', unread=True): 94 | self.assertIsNotNone(convo.id) 95 | 96 | def test_find_single_conversation(self): 97 | # FINDING A SINGLE CONVERSATION 98 | convo_id = intercom.conversations.find_all(type='admin', id=self.admin.id)[0].id # noqa 99 | conversation = intercom.conversations.find(id=convo_id) 100 | self.assertEqual(conversation.id, convo_id) 101 | 102 | def test_conversation_parts(self): 103 | # INTERACTING WITH THE PARTS OF A CONVERSATION 104 | convo_id = intercom.conversations.find_all(type='admin', id=self.admin.id)[0].id # noqa 105 | conversation = intercom.conversations.find(id=convo_id) 106 | 107 | # Getting the subject of a part (only applies to email-based 108 | # conversations) 109 | self.assertEqual(conversation.conversation_message.subject, "") 110 | for part in conversation.conversation_parts: 111 | # There is a part_type 112 | self.assertIsNotNone(part.part_type) 113 | # There is a body 114 | if not part.part_type == 'assignment': 115 | self.assertIsNotNone(part.body) 116 | 117 | def test_a_reply(self): 118 | # REPLYING TO CONVERSATIONS 119 | conversation = intercom.conversations.find(id=self.admin_conv.id) 120 | num_parts = len(conversation.conversation_parts) 121 | # User (identified by email) replies with a comment 122 | intercom.conversations.reply( 123 | id=conversation.id, 124 | type='user', email=self.email, 125 | message_type='comment', body='foo') 126 | # Admin (identified by admin_id) replies with a comment 127 | intercom.conversations.reply( 128 | id=conversation.id, 129 | type='admin', admin_id=self.admin.id, 130 | message_type='comment', body='bar') 131 | conversation = intercom.conversations.find(id=self.admin_conv.id) 132 | self.assertEqual(num_parts + 2, len(conversation.conversation_parts)) 133 | 134 | def test_open(self): 135 | # OPENING CONVERSATIONS 136 | conversation = intercom.conversations.find(id=self.admin_conv.id) 137 | intercom.conversations.close( 138 | id=conversation.id, admin_id=self.admin.id, body='Closing message') # noqa 139 | self.assertFalse(conversation.open) 140 | intercom.conversations.open( 141 | id=conversation.id, admin_id=self.admin.id, body='Opening message') # noqa 142 | conversation = intercom.conversations.find(id=self.admin_conv.id) 143 | self.assertTrue(conversation.open) 144 | 145 | def test_close(self): 146 | # CLOSING CONVERSATIONS 147 | conversation = intercom.conversations.find(id=self.admin_conv.id) 148 | self.assertTrue(conversation.open) 149 | intercom.conversations.close( 150 | id=conversation.id, admin_id=self.admin.id, body='Closing message') # noqa 151 | conversation = intercom.conversations.find(id=self.admin_conv.id) 152 | self.assertFalse(conversation.open) 153 | 154 | def test_assignment(self): 155 | # ASSIGNING CONVERSATIONS 156 | conversation = intercom.conversations.find(id=self.admin_conv.id) 157 | num_parts = len(conversation.conversation_parts) 158 | intercom.conversations.assign( 159 | id=conversation.id, assignee_id=self.admin.id, 160 | admin_id=self.admin.id) 161 | conversation = intercom.conversations.find(id=self.admin_conv.id) 162 | self.assertEqual(num_parts + 1, len(conversation.conversation_parts)) 163 | self.assertEqual("assignment", conversation.conversation_parts[-1].part_type) # noqa 164 | 165 | def test_mark_read(self): 166 | # MARKING A CONVERSATION AS READ 167 | conversation = intercom.conversations.find(id=self.admin_conv.id) 168 | conversation.read = False 169 | intercom.conversations.save(conversation) 170 | conversation = intercom.conversations.find(id=self.admin_conv.id) 171 | self.assertFalse(conversation.read) 172 | conversation.read = True 173 | intercom.conversations.save(conversation) 174 | conversation = intercom.conversations.find(id=self.admin_conv.id) 175 | self.assertTrue(conversation.read) 176 | -------------------------------------------------------------------------------- /tests/integration/test_count.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Integration test for Intercom Counts.""" 3 | 4 | import os 5 | import unittest 6 | from intercom.client import Client 7 | from nose.tools import eq_ 8 | from nose.tools import ok_ 9 | from . import delete_company 10 | from . import delete_user 11 | from . import get_timestamp 12 | from . import get_or_create_company 13 | from . import get_or_create_user 14 | 15 | intercom = Client( 16 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 17 | 18 | 19 | class CountTest(unittest.TestCase): 20 | 21 | @classmethod 22 | def setup_class(cls): 23 | nowstamp = get_timestamp() 24 | cls.company = get_or_create_company(intercom, nowstamp) 25 | cls.user = get_or_create_user(intercom, nowstamp) 26 | 27 | @classmethod 28 | def teardown_class(cls): 29 | delete_company(intercom, cls.company) 30 | delete_user(intercom, cls.user) 31 | 32 | def test_user_counts_for_each_tag(self): 33 | # Get User Tag Count Object 34 | intercom.tags.tag(name='blue', users=[{'id': self.user.id}]) 35 | counts = intercom.counts.for_type(type='user', count='tag') 36 | intercom.tags.untag(name='blue', users=[{'id': self.user.id}]) 37 | for count in counts.user['tag']: 38 | if 'blue' in count: 39 | eq_(count['blue'], 1) 40 | 41 | def test_user_counts_for_each_segment(self): 42 | # Get User Segment Count Object 43 | counts = intercom.counts.for_type(type='user', count='segment') 44 | ok_(counts) 45 | 46 | def test_company_counts_for_each_segment(self): 47 | # Get Company Segment Count Object 48 | counts = intercom.counts.for_type(type='company', count='segment') 49 | ok_(counts) 50 | 51 | def test_company_counts_for_each_tag(self): 52 | # Get Company Tag Count Object 53 | intercom.tags.tag(name='blue', companies=[{'id': self.company.id}]) 54 | intercom.counts.for_type(type='company', count='tag') 55 | intercom.tags.untag(name='blue', companies=[{'id': self.company.id}]) 56 | 57 | def test_company_counts_for_each_user(self): 58 | # Get Company User Count Object 59 | self.user.companies = [ 60 | {"company_id": self.company.company_id} 61 | ] 62 | intercom.users.save(self.user) 63 | counts = intercom.counts.for_type(type='company', count='user') 64 | for count in counts.company['user']: 65 | if self.company.name in count: 66 | eq_(count[self.company.name], 1) 67 | 68 | def test_global(self): 69 | counts = intercom.counts.for_app() 70 | ok_(counts.company >= 0) 71 | ok_(counts.tag >= 0) 72 | ok_(counts.segment >= 0) 73 | ok_(counts.user >= 0) 74 | ok_(counts.lead >= 0) 75 | -------------------------------------------------------------------------------- /tests/integration/test_notes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | from . import delete_user 7 | from . import get_or_create_user 8 | from . import get_timestamp 9 | 10 | intercom = Client( 11 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 12 | 13 | 14 | class NoteTest(unittest.TestCase): 15 | 16 | @classmethod 17 | def setup_class(cls): 18 | timestamp = get_timestamp() 19 | cls.user = get_or_create_user(intercom, timestamp) 20 | cls.email = cls.user.email 21 | 22 | @classmethod 23 | def teardown_class(cls): 24 | delete_user(intercom, cls.user) 25 | 26 | def test_create_note(self): 27 | # Create a note for a user 28 | note = intercom.notes.create( 29 | body="

Text for the note

", 30 | email=self.email) 31 | self.assertIsNotNone(note.id) 32 | 33 | def test_find_note(self): 34 | # Find a note by id 35 | orig_note = intercom.notes.create( 36 | body="

Text for the note

", 37 | email=self.email) 38 | note = intercom.notes.find(id=orig_note.id) 39 | self.assertEqual(note.body, orig_note.body) 40 | 41 | def test_find_all_email(self): 42 | # Iterate over all notes for a user via their email address 43 | notes = intercom.notes.find_all(email=self.email) 44 | for note in notes: 45 | self.assertTrue(note.id is not None) 46 | user = intercom.users.load(note.user) 47 | self.assertEqual(user.email, self.email) 48 | break 49 | 50 | def test_find_all_id(self): 51 | user = intercom.users.find(email=self.email) 52 | 53 | # Iterate over all notes for a user via their email address 54 | for note in intercom.notes.find_all(user_id=user.user_id): 55 | self.assertTrue(note.id is not None) 56 | user = intercom.users.load(note.user) 57 | self.assertEqual(user.email, self.email) 58 | -------------------------------------------------------------------------------- /tests/integration/test_segments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | 7 | intercom = Client( 8 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 9 | 10 | 11 | class SegmentTest(unittest.TestCase): 12 | 13 | @classmethod 14 | def setup_class(cls): 15 | cls.segment = intercom.segments.all()[0] 16 | 17 | def test_find_segment(self): 18 | # Find a segment 19 | segment = intercom.segments.find(id=self.segment.id) 20 | self.assertEqual(segment.id, self.segment.id) 21 | 22 | def test_iterate(self): 23 | # Iterate over all segments 24 | for segment in intercom.segments.all(): 25 | self.assertTrue(segment.id is not None) 26 | -------------------------------------------------------------------------------- /tests/integration/test_tags.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | from . import delete_user 7 | from . import delete_company 8 | from . import get_or_create_company 9 | from . import get_or_create_user 10 | from . import get_timestamp 11 | 12 | intercom = Client( 13 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 14 | 15 | 16 | class TagTest(unittest.TestCase): 17 | 18 | @classmethod 19 | def setup_class(cls): 20 | nowstamp = get_timestamp() 21 | cls.company = get_or_create_company(intercom, nowstamp) 22 | cls.user = get_or_create_user(intercom, nowstamp) 23 | cls.user.companies = [ 24 | {"company_id": cls.company.id, "name": cls.company.name} 25 | ] 26 | intercom.users.save(cls.user) 27 | 28 | @classmethod 29 | def teardown_class(cls): 30 | delete_company(intercom, cls.company) 31 | delete_user(intercom, cls.user) 32 | 33 | def test_tag_users(self): 34 | # Tag users 35 | tag = intercom.tags.tag(name='blue', users=[{'id': self.user.id}]) 36 | self.assertEqual(tag.name, 'blue') 37 | user = intercom.users.find(email=self.user.email) 38 | self.assertEqual(1, len(user.tags)) 39 | 40 | def test_untag_users(self): 41 | # Untag users 42 | tag = intercom.tags.untag(name='blue', users=[{'id': self.user.id}]) 43 | self.assertEqual(tag.name, 'blue') 44 | user = intercom.users.find(email=self.user.email) 45 | self.assertEqual(0, len(user.tags)) 46 | 47 | def test_all(self): 48 | # Iterate over all tags 49 | for tag in intercom.tags.all(): 50 | self.assertIsNotNone(tag.id) 51 | 52 | def test_tag_companies(self): 53 | # Tag companies 54 | tag = intercom.tags.tag( 55 | name="blue", companies=[{'id': self.user.companies[0].id}]) 56 | self.assertEqual(tag.name, "blue") 57 | company = intercom.companies.find(id=self.user.companies[0].id) 58 | self.assertEqual(1, len(company.tags)) 59 | 60 | def test_untag_companies(self): 61 | # Untag companies 62 | tag = intercom.tags.untag( 63 | name="blue", companies=[{'id': self.user.companies[0].id}]) 64 | self.assertEqual(tag.name, "blue") 65 | company = intercom.companies.find(id=self.user.companies[0].id) 66 | self.assertEqual(0, len(company.tags)) 67 | -------------------------------------------------------------------------------- /tests/integration/test_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import unittest 5 | from intercom.client import Client 6 | from . import get_timestamp 7 | from . import get_or_create_user 8 | from . import delete_user 9 | 10 | intercom = Client( 11 | os.environ.get('INTERCOM_PERSONAL_ACCESS_TOKEN')) 12 | 13 | 14 | class UserTest(unittest.TestCase): 15 | 16 | @classmethod 17 | def setup_class(cls): 18 | nowstamp = get_timestamp() 19 | cls.user = get_or_create_user(intercom, nowstamp) 20 | cls.email = cls.user.email 21 | 22 | @classmethod 23 | def teardown_class(cls): 24 | delete_user(intercom, cls.user) 25 | 26 | def test_find_by_email(self): 27 | # Find user by email 28 | user = intercom.users.find(email=self.email) 29 | self.assertEqual(self.email, user.email) 30 | 31 | def test_find_by_user_id(self): 32 | # Find user by user id 33 | user = intercom.users.find(user_id=self.user.user_id) 34 | self.assertEqual(self.email, user.email) 35 | 36 | def test_find_by_id(self): 37 | # Find user by id 38 | user = intercom.users.find(id=self.user.id) 39 | self.assertEqual(self.email, user.email) 40 | 41 | def test_custom_attributes(self): 42 | # Update custom_attributes for a user 43 | user = intercom.users.find(id=self.user.id) 44 | user.custom_attributes["average_monthly_spend"] = 1234.56 45 | intercom.users.save(user) 46 | user = intercom.users.find(id=self.user.id) 47 | self.assertEqual( 48 | user.custom_attributes["average_monthly_spend"], 1234.56) 49 | 50 | def test_increment(self): 51 | # Perform incrementing 52 | user = intercom.users.find(id=self.user.id) 53 | karma = user.custom_attributes.get('karma', 0) 54 | user.increment('karma') 55 | intercom.users.save(user) 56 | self.assertEqual(user.custom_attributes["karma"], karma + 1) 57 | user.increment('karma') 58 | intercom.users.save(user) 59 | self.assertEqual(user.custom_attributes["karma"], karma + 2) 60 | user.custom_attributes['logins'] = None 61 | user.increment('logins') 62 | intercom.users.save(user) 63 | self.assertEqual(user.custom_attributes['logins'], 1) 64 | 65 | def test_iterate(self): 66 | # Iterate over all users 67 | for user in intercom.users.all(): 68 | self.assertTrue(user.id is not None) 69 | -------------------------------------------------------------------------------- /tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | nosetests tests/unit 3 | exit $? 4 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | import os 5 | 6 | from mock import Mock 7 | 8 | DIRPATH = os.path.dirname(__file__) 9 | FIXTURES = os.path.join(DIRPATH, 'fixtures') 10 | 11 | 12 | def create_response(status, fixture=None): 13 | def request(*args, **kwargs): 14 | response = Mock() 15 | response.status_code = status 16 | if fixture: 17 | fixture_path = os.path.join(FIXTURES, fixture) 18 | response.content = open(fixture_path).read() 19 | return response 20 | return request 21 | 22 | 23 | def local_response(**params): 24 | def _call(*args, **kwargs): 25 | response = Mock() 26 | reply = {} 27 | for name, value in list(kwargs.items()): 28 | reply[name] = value 29 | for name, value in list(params.items()): 30 | reply[name] = value 31 | response.content = json.dumps(reply) 32 | response.status_code = 200 33 | return response 34 | return _call 35 | 36 | 37 | def mock_response(content, status_code=200, encoding='utf-8', headers=None): 38 | if headers is None: 39 | headers = { 40 | 'x-ratelimit-limit': 500, 41 | 'x-ratelimit-remaining': 500, 42 | 'x-ratelimit-reset': 1427932858 43 | } 44 | return Mock( 45 | content=content, status_code=status_code, encoding=encoding, headers=headers) 46 | 47 | 48 | def get_user(email="bob@example.com", name="Joe Schmoe"): 49 | return { 50 | "type": "user", 51 | "id": "aaaaaaaaaaaaaaaaaaaaaaaa", 52 | "user_id": 'id-from-customers-app', 53 | "email": email, 54 | "name": name, 55 | "avatar": { 56 | "type": "avatar", 57 | "image_url": "https://graph.facebook.com/1/picture?width=24&height=24" 58 | }, 59 | "app_id": "the-app-id", 60 | "created_at": 1323422442, 61 | "custom_attributes": {"a": "b", "b": 2}, 62 | "companies": { 63 | "type": "company.list", 64 | "companies": [ 65 | { 66 | "type": "company", 67 | "company_id": "123", 68 | "id": "bbbbbbbbbbbbbbbbbbbbbbbb", 69 | "app_id": "the-app-id", 70 | "name": "Company 1", 71 | "remote_created_at": 1390936440, 72 | "created_at": 1401970114, 73 | "updated_at": 1401970114, 74 | "last_request_at": 1401970113, 75 | "monthly_spend": 0, 76 | "session_count": 0, 77 | "user_count": 1, 78 | "tag_ids": [], 79 | "custom_attributes": { 80 | "category": "Tech" 81 | } 82 | } 83 | ] 84 | }, 85 | "session_count": 123, 86 | "unsubscribed_from_emails": True, 87 | "last_request_at": 1401970113, 88 | "created_at": 1401970114, 89 | "remote_created_at": 1393613864, 90 | "updated_at": 1401970114, 91 | "user_agent_data": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11", 92 | "social_profiles": { 93 | "type": "social_profile.list", 94 | "social_profiles": [ 95 | { 96 | "type": "social_profile", 97 | "name": "twitter", 98 | "url": "http://twitter.com/abc", 99 | "username": "abc", 100 | "id": None 101 | }, 102 | { 103 | "type": "social_profile", 104 | "name": "twitter", 105 | "username": "abc2", 106 | "url": "http://twitter.com/abc2", 107 | "id": None 108 | }, 109 | { 110 | "type": "social_profile", 111 | "name": "facebook", 112 | "url": "http://facebook.com/abc", 113 | "username": "abc", 114 | "id": "1234242" 115 | }, 116 | { 117 | "type": "social_profile", 118 | "name": "quora", 119 | "url": "http://facebook.com/abc", 120 | "username": "abc", 121 | "id": "1234242" 122 | } 123 | ] 124 | }, 125 | "location_data": { 126 | "type": "location_data", 127 | "city_name": 'Dublin', 128 | "continent_code": 'EU', 129 | "country_name": 'Ireland', 130 | "latitude": '90', 131 | "longitude": '10', 132 | "postal_code": 'IE', 133 | "region_name": 'Europe', 134 | "timezone": '+1000', 135 | "country_code": "IRL" 136 | } 137 | } 138 | 139 | 140 | def get_company(name): 141 | return { 142 | "type": "company", 143 | "id": "531ee472cce572a6ec000006", 144 | "name": name, 145 | "plan": { 146 | "type": "plan", 147 | "id": "1", 148 | "name": "Paid" 149 | }, 150 | "company_id": "6", 151 | "remote_created_at": 1394531169, 152 | "created_at": 1394533506, 153 | "updated_at": 1396874658, 154 | "monthly_spend": 49, 155 | "session_count": 26, 156 | "user_count": 10, 157 | "custom_attributes": { 158 | "paid_subscriber": True, 159 | "team_mates": 0 160 | } 161 | } 162 | 163 | 164 | def get_event(name="the-event-name"): 165 | return { 166 | "type": "event", 167 | "event_name": name, 168 | "created_at": 1389913941, 169 | "user_id": "314159", 170 | "metadata": { 171 | "type": "user", 172 | "invitee_email": "pi@example.org", 173 | "invite_code": "ADDAFRIEND" 174 | } 175 | } 176 | 177 | 178 | def page_of_users(include_next_link=False): 179 | page = { 180 | "type": "user.list", 181 | "pages": { 182 | "type": "pages", 183 | "page": 1, 184 | "next": None, 185 | "per_page": 50, 186 | "total_pages": 7 187 | }, 188 | "users": [ 189 | get_user("user1@example.com"), 190 | get_user("user2@example.com"), 191 | get_user("user3@example.com")], 192 | "total_count": 314 193 | } 194 | if include_next_link: 195 | page["pages"]["next"] = "https://api.intercom.io/users?per_page=50&page=2" 196 | return page 197 | 198 | 199 | def users_scroll(include_users=False): # noqa 200 | # a "page" of results from the Scroll API 201 | if include_users: 202 | users = [ 203 | get_user("user1@example.com"), 204 | get_user("user2@example.com"), 205 | get_user("user3@example.com") 206 | ] 207 | else: 208 | users = [] 209 | 210 | return { 211 | "type": "user.list", 212 | "scroll_param": "da6bbbac-25f6-4f07-866b-b911082d7", 213 | "users": users 214 | } 215 | 216 | 217 | def page_of_events(include_next_link=False): 218 | page = { 219 | "type": "event.list", 220 | "pages": { 221 | "next": None, 222 | }, 223 | "events": [ 224 | get_event("invited-friend"), 225 | get_event("bought-sub")], 226 | } 227 | if include_next_link: 228 | page["pages"]["next"] = "https://api.intercom.io/events?type=user&intercom_user_id=55a3b&before=144474756550" # noqa 229 | return page 230 | 231 | 232 | def page_of_companies(include_next_link=False): 233 | page = { 234 | "type": "company.list", 235 | "pages": { 236 | "type": "pages", 237 | "page": 1, 238 | "next": None, 239 | "per_page": 50, 240 | "total_pages": 7 241 | }, 242 | "companies": [ 243 | get_company('ACME A'), 244 | get_company('ACME B'), 245 | get_company('ACME C') 246 | ], 247 | "total_count": 3 248 | } 249 | if include_next_link: 250 | page["pages"]["next"] = "https://api.intercom.io/companies?per_page=50&page=2" 251 | return page 252 | 253 | 254 | test_tag = { 255 | "id": "4f73428b5e4dfc000b000112", 256 | "name": "Test Tag", 257 | "segment": False, 258 | "tagged_user_count": 2 259 | } 260 | 261 | test_subscription = { 262 | "type": "notification_subscription", 263 | "id": "nsub_123456789", 264 | "created_at": 1410368642, 265 | "updated_at": 1410368642, 266 | "service_type": "web", 267 | "app_id": "3qmk5gyg", 268 | "url": "http://example.com", 269 | "self": "https://api.intercom.io/subscriptions/nsub_123456789", 270 | "topics": ["user.created", "conversation.user.replied", "conversation.admin.replied"], 271 | "active": True, 272 | "metadata": {}, 273 | "hub_secret": None, 274 | "mode": "point", 275 | "links": { 276 | "sent": "https://api.intercom.io/subscriptions/nsub_123456789/sent", 277 | "retry": "https://api.intercom.io/subscriptions/nsub_123456789/retry", 278 | "errors": "https://api.intercom.io/subscriptions/nsub_123456789/errors" 279 | }, 280 | "notes": [] 281 | } 282 | 283 | test_user_notification = { 284 | "type": "notification_event", 285 | "id": "notif_123456-56465-546546", 286 | "topic": "user.created", 287 | "app_id": "aaaaaa", 288 | "data": { 289 | "type": "notification_event_data", 290 | "item": { 291 | "type": "user", 292 | "id": "aaaaaaaaaaaaaaaaaaaaaaaa", 293 | "user_id": None, 294 | "email": "joe@example.com", 295 | "name": "Joe Schmoe", 296 | "avatar": { 297 | "type": "avatar", 298 | "image_url": None 299 | }, 300 | "app_id": "aaaaa", 301 | "companies": { 302 | "type": "company.list", 303 | "companies": [] 304 | }, 305 | "location_data": { 306 | }, 307 | "last_request_at": None, 308 | "created_at": "1401970114", 309 | "remote_created_at": None, 310 | "updated_at": "1401970114", 311 | "session_count": 0, 312 | "social_profiles": { 313 | "type": "social_profile.list", 314 | "social_profiles": [] 315 | }, 316 | "unsubscribed_from_emails": False, 317 | "user_agent_data": None, 318 | "tags": { 319 | "type": "tag.list", 320 | "tags": [] 321 | }, 322 | "segments": { 323 | "type": "segment.list", 324 | "segments": [] 325 | }, 326 | "custom_attributes": { 327 | } 328 | } 329 | }, 330 | "delivery_status": None, 331 | "delivery_attempts": 1, 332 | "delivered_at": 0, 333 | "first_sent_at": 1410188629, 334 | "created_at": 1410188628, 335 | "links": {}, 336 | "self": None 337 | } 338 | 339 | test_conversation_notification = { 340 | "type": "notification_event", 341 | "id": "notif_123456-56465-546546", 342 | "topic": "conversation.user.created", 343 | "app_id": "aaaaa", 344 | "data": { 345 | "type": "notification_event_data", 346 | "item": { 347 | "type": "conversation", 348 | "id": "123456789", 349 | "created_at": "1410335293", 350 | "updated_at": "1410335293", 351 | "user": { 352 | "type": "user", 353 | "id": "540f1de7112d3d1d51001637", 354 | "name": "Kill Bill", 355 | "email": "bill@bill.bill" 356 | }, 357 | "assignee": { 358 | "type": "nobody_admin", 359 | "id": None 360 | }, 361 | "conversation_message": { 362 | "type": "conversation_message", 363 | "id": "321546", 364 | "subject": "", 365 | "body": "

An important message

", 366 | "author": { 367 | "type": "user", 368 | "id": "aaaaaaaaaaaaaaaaaaaaaa", 369 | "name": "Kill Bill", 370 | "email": "bill@bill.bill" 371 | }, 372 | "attachments": [] 373 | }, 374 | "conversation_parts": { 375 | "type": "conversation_part.list", 376 | "conversation_parts": [ 377 | { 378 | "type": "conversation_part", 379 | "id": "4412", 380 | "part_type": "comment", 381 | "body": "

Hi Jane, it's all great thanks!

", 382 | "created_at": 1400857494, 383 | "updated_at": 1400857494, 384 | "notified_at": 1400857587, 385 | "assigned_to": None, 386 | "author": { 387 | "type": "user", 388 | "id": "536e564f316c83104c000020" 389 | }, 390 | "attachments": [] 391 | } 392 | ] 393 | }, 394 | "open": None, 395 | "read": True, 396 | "links": { 397 | "conversation_web": "https://app.intercom.io/a/apps/aaaaaa/inbox/all/conversations/123456789" 398 | } 399 | } 400 | }, 401 | "delivery_status": None, 402 | "delivery_attempts": 1, 403 | "delivered_at": 0, 404 | "first_sent_at": 1410335293, 405 | "created_at": 1410335293, 406 | "links": {}, 407 | "self": "http://example.com/resource/url/" 408 | } 409 | -------------------------------------------------------------------------------- /tests/unit/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/unit/lib/test_flat_store.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | from intercom.lib.flat_store import FlatStore 5 | from nose.tools import assert_raises 6 | from nose.tools import eq_ 7 | from nose.tools import istest 8 | 9 | 10 | class IntercomFlatStore(unittest.TestCase): 11 | 12 | @istest 13 | def it_raises_if_you_try_to_set_or_merge_in_nested_hash_structures(self): 14 | data = FlatStore() 15 | with assert_raises(ValueError): 16 | data["thing"] = [1] 17 | with assert_raises(ValueError): 18 | data["thing"] = {1: 2} 19 | with assert_raises(ValueError): 20 | FlatStore(**{"1": {2: 3}}) 21 | 22 | @istest 23 | def it_raises_if_you_try_to_use_a_non_string_key(self): 24 | data = FlatStore() 25 | with assert_raises(ValueError): 26 | data[1] = "something" 27 | 28 | @istest 29 | def it_sets_and_merges_valid_entries(self): 30 | data = FlatStore() 31 | data["a"] = 1 32 | data["b"] = 2 33 | eq_(data["a"], 1) 34 | eq_(data["b"], 2) 35 | data = FlatStore(a=1, b=2) 36 | eq_(data["a"], 1) 37 | eq_(data["b"], 2) 38 | 39 | @istest 40 | def it_sets_null_entries(self): 41 | data = FlatStore() 42 | data["a"] = None 43 | eq_(data["a"], None) 44 | -------------------------------------------------------------------------------- /tests/unit/test_admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from intercom.request import Request 6 | from intercom.client import Client 7 | from intercom.collection_proxy import CollectionProxy 8 | from mock import patch 9 | from nose.tools import assert_raises 10 | from nose.tools import istest 11 | 12 | 13 | def send_request(*args, **kwargs): 14 | # empty impl 15 | raise (AssertionError) 16 | 17 | 18 | class AdminTest(unittest.TestCase): 19 | 20 | @istest 21 | @patch.object(Request, 'send_request_to_path', send_request) 22 | def it_returns_a_collection_proxy_for_all_without_making_any_requests(self): # noqa 23 | client = Client() 24 | # prove a call to send_request_to_path will raise an error 25 | with assert_raises(AssertionError): 26 | send_request() 27 | all = client.admins.all() 28 | self.assertIsInstance(all, CollectionProxy) 29 | -------------------------------------------------------------------------------- /tests/unit/test_collection_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from intercom.client import Client 6 | from mock import call 7 | from mock import patch 8 | from nose.tools import eq_ 9 | from nose.tools import istest 10 | from tests.unit import page_of_users 11 | 12 | 13 | class CollectionProxyTest(unittest.TestCase): 14 | 15 | def setUp(self): 16 | self.client = Client() 17 | 18 | @istest 19 | def it_stops_iterating_if_no_next_link(self): 20 | body = page_of_users(include_next_link=False) 21 | with patch.object(Client, 'get', return_value=body) as mock_method: 22 | emails = [user.email for user in self.client.users.all()] 23 | mock_method.assert_called_once_with('/users', {}) 24 | eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com']) # noqa 25 | 26 | @istest 27 | def it_keeps_iterating_if_next_link(self): 28 | page1 = page_of_users(include_next_link=True) 29 | page2 = page_of_users(include_next_link=False) 30 | side_effect = [page1, page2] 31 | with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa 32 | emails = [user.email for user in self.client.users.all()] 33 | eq_([call('/users', {}), call('/users?per_page=50&page=2', {})], # noqa 34 | mock_method.mock_calls) 35 | eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com'] * 2) # noqa 36 | 37 | @istest 38 | def it_supports_indexed_array_access(self): 39 | body = page_of_users(include_next_link=False) 40 | with patch.object(Client, 'get', return_value=body) as mock_method: 41 | eq_(self.client.users.all()[0].email, 'user1@example.com') 42 | mock_method.assert_called_once_with('/users', {}) 43 | 44 | @istest 45 | def it_supports_querying(self): 46 | body = page_of_users(include_next_link=False) 47 | with patch.object(Client, 'get', return_value=body) as mock_method: 48 | emails = [user.email for user in self.client.users.find_all(tag_name='Taggart J')] # noqa 49 | eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com']) # noqa 50 | mock_method.assert_called_once_with('/users', {'tag_name': 'Taggart J'}) # noqa 51 | -------------------------------------------------------------------------------- /tests/unit/test_company.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # noqa 2 | 3 | import intercom 4 | import unittest 5 | 6 | from intercom.client import Client 7 | from intercom.company import Company 8 | from mock import call 9 | from mock import patch 10 | from nose.tools import assert_raises 11 | from nose.tools import eq_ 12 | from nose.tools import ok_ 13 | from nose.tools import istest 14 | from tests.unit import page_of_companies 15 | 16 | 17 | class CompanyTest(unittest.TestCase): # noqa 18 | 19 | def setUp(self): # noqa 20 | self.client = Client() 21 | 22 | @istest 23 | def it_raises_error_if_no_response_on_find(self): # noqa 24 | with patch.object(Client, 'get', return_value=None) as mock_method: 25 | with assert_raises(intercom.HttpError): 26 | self.client.companies.find(company_id='4') 27 | mock_method.assert_called_once_with('/companies', {'company_id': '4'}) 28 | 29 | @istest 30 | def it_raises_error_if_no_response_on_find_all(self): # noqa 31 | with patch.object(Client, 'get', return_value=None) as mock_method: 32 | with assert_raises(intercom.HttpError): 33 | [x for x in self.client.companies.all()] 34 | mock_method.assert_called_once_with('/companies', {}) 35 | 36 | @istest 37 | def it_raises_error_on_load(self): # noqa 38 | company = Company() 39 | company.id = '4' 40 | side_effect = [None] 41 | with patch.object(Client, 'get', side_effect=side_effect) as mock_method: 42 | with assert_raises(intercom.HttpError): 43 | self.client.companies.load(company) 44 | eq_([call('/companies/4', {})], mock_method.mock_calls) 45 | 46 | @istest 47 | def it_gets_companies_by_tag(self): # noqa 48 | with patch.object(Client, 'get', return_value=page_of_companies(False)) as mock_method: 49 | companies = self.client.companies.by_tag(124) 50 | for company in companies: 51 | ok_(hasattr(company, 'company_id')) 52 | eq_([call('/companies?tag_id=124', {})], mock_method.mock_calls) 53 | -------------------------------------------------------------------------------- /tests/unit/test_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import unittest 5 | 6 | from datetime import datetime 7 | from intercom.client import Client 8 | from intercom.user import User 9 | from mock import call 10 | from mock import patch 11 | from nose.tools import eq_ 12 | from nose.tools import istest 13 | from tests.unit import page_of_events 14 | 15 | 16 | class EventTest(unittest.TestCase): 17 | 18 | def setUp(self): # noqa 19 | self.client = Client() 20 | now = time.mktime(datetime.utcnow().timetuple()) 21 | self.user = User( 22 | email="jim@example.com", 23 | user_id="12345", 24 | created_at=now, 25 | name="Jim Bob") 26 | self.created_time = now - 300 27 | 28 | @istest 29 | def it_stops_iterating_if_no_next_link(self): 30 | body = page_of_events(include_next_link=False) 31 | with patch.object(Client, 'get', return_value=body) as mock_method: # noqa 32 | event_names = [event.event_name for event in self.client.events.find_all( 33 | type='user', email='joe@example.com')] 34 | mock_method.assert_called_once_with( 35 | '/events', {'type': 'user', 'email': 'joe@example.com'}) 36 | eq_(event_names, ['invited-friend', 'bought-sub']) # noqa 37 | 38 | @istest 39 | def it_keeps_iterating_if_next_link(self): 40 | page1 = page_of_events(include_next_link=True) 41 | page2 = page_of_events(include_next_link=False) 42 | side_effect = [page1, page2] 43 | with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa 44 | event_names = [event.event_name for event in self.client.events.find_all( 45 | type='user', email='joe@example.com')] 46 | eq_([call('/events', {'type': 'user', 'email': 'joe@example.com'}), 47 | call('/events?type=user&intercom_user_id=55a3b&before=144474756550', {})], # noqa 48 | mock_method.mock_calls) 49 | eq_(event_names, ['invited-friend', 'bought-sub'] * 2) # noqa 50 | 51 | @istest 52 | def it_creates_an_event_with_metadata(self): 53 | data = { 54 | 'event_name': 'Eventful 1', 55 | 'created_at': self.created_time, 56 | 'email': 'joe@example.com', 57 | 'metadata': { 58 | 'invitee_email': 'pi@example.com', 59 | 'invite_code': 'ADDAFRIEND', 60 | 'found_date': 12909364407 61 | } 62 | } 63 | 64 | with patch.object(Client, 'post', return_value=data) as mock_method: 65 | self.client.events.create(**data) 66 | mock_method.assert_called_once_with('/events/', data) 67 | 68 | @istest 69 | def it_creates_an_event_without_metadata(self): 70 | data = { 71 | 'event_name': 'sale of item', 72 | 'email': 'joe@example.com', 73 | } 74 | with patch.object(Client, 'post', return_value=data) as mock_method: 75 | self.client.events.create(**data) 76 | mock_method.assert_called_once_with('/events/', data) 77 | 78 | class DescribeBulkOperations(unittest.TestCase): # noqa 79 | def setUp(self): # noqa 80 | self.client = Client() 81 | 82 | self.job = { 83 | "app_id": "app_id", 84 | "id": "super_awesome_job", 85 | "created_at": 1446033421, 86 | "completed_at": 1446048736, 87 | "closing_at": 1446034321, 88 | "updated_at": 1446048736, 89 | "name": "api_bulk_job", 90 | "state": "completed", 91 | "links": { 92 | "error": "https://api.intercom.io/jobs/super_awesome_job/error", 93 | "self": "https://api.intercom.io/jobs/super_awesome_job" 94 | }, 95 | "tasks": [ 96 | { 97 | "id": "super_awesome_task", 98 | "item_count": 2, 99 | "created_at": 1446033421, 100 | "started_at": 1446033709, 101 | "completed_at": 1446033709, 102 | "state": "completed" 103 | } 104 | ] 105 | } 106 | 107 | self.bulk_request = { 108 | "items": [ 109 | { 110 | "method": "post", 111 | "data_type": "event", 112 | "data": { 113 | "event_name": "ordered-item", 114 | "created_at": 1438944980, 115 | "user_id": "314159", 116 | "metadata": { 117 | "order_date": 1438944980, 118 | "stripe_invoice": "inv_3434343434" 119 | } 120 | } 121 | }, 122 | { 123 | "method": "post", 124 | "data_type": "event", 125 | "data": { 126 | "event_name": "invited-friend", 127 | "created_at": 1438944979, 128 | "user_id": "314159", 129 | "metadata": { 130 | "invitee_email": "pi@example.org", 131 | "invite_code": "ADDAFRIEND" 132 | } 133 | } 134 | } 135 | ] 136 | } 137 | 138 | self.events = [ 139 | { 140 | "event_name": "ordered-item", 141 | "created_at": 1438944980, 142 | "user_id": "314159", 143 | "metadata": { 144 | "order_date": 1438944980, 145 | "stripe_invoice": "inv_3434343434" 146 | } 147 | }, 148 | { 149 | "event_name": "invited-friend", 150 | "created_at": 1438944979, 151 | "user_id": "314159", 152 | "metadata": { 153 | "invitee_email": "pi@example.org", 154 | "invite_code": "ADDAFRIEND" 155 | } 156 | } 157 | ] 158 | 159 | @istest 160 | def it_submits_a_bulk_job(self): # noqa 161 | with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa 162 | self.client.events.submit_bulk_job(create_items=self.events) 163 | mock_method.assert_called_once_with('/bulk/events', self.bulk_request) 164 | 165 | @istest 166 | def it_adds_events_to_an_existing_bulk_job(self): # noqa 167 | self.bulk_request['job'] = {'id': 'super_awesome_job'} 168 | with patch.object(Client, 'post', return_value=self.job) as mock_method: # noqa 169 | self.client.events.submit_bulk_job( 170 | create_items=self.events, job_id='super_awesome_job') 171 | mock_method.assert_called_once_with('/bulk/events', self.bulk_request) 172 | 173 | @istest 174 | def it_does_not_submit_delete_jobs(self): # noqa 175 | with self.assertRaises(Exception): 176 | self.client.events.submit_bulk_job(delete_items=self.events) 177 | -------------------------------------------------------------------------------- /tests/unit/test_import.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # License: http://jkeyes.mit-license.org/ 4 | # 5 | from intercom import * 6 | 7 | 8 | def test_wildcard_import(): 9 | pass 10 | -------------------------------------------------------------------------------- /tests/unit/test_job.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # noqa 2 | 3 | import unittest 4 | 5 | from intercom.client import Client 6 | from mock import patch 7 | from nose.tools import istest 8 | 9 | 10 | class DescribeJobs(unittest.TestCase): # noqa 11 | def setUp(self): # noqa 12 | self.client = Client() 13 | 14 | self.job = { 15 | "app_id": "app_id", 16 | "id": "super_awesome_job", 17 | "created_at": 1446033421, 18 | "completed_at": 1446048736, 19 | "closing_at": 1446034321, 20 | "updated_at": 1446048736, 21 | "name": "api_bulk_job", 22 | "state": "completed", 23 | "links": { 24 | "error": "https://api.intercom.io/jobs/super_awesome_job/error", 25 | "self": "https://api.intercom.io/jobs/super_awesome_job" 26 | }, 27 | "tasks": [ 28 | { 29 | "id": "super_awesome_task", 30 | "item_count": 2, 31 | "created_at": 1446033421, 32 | "started_at": 1446033709, 33 | "completed_at": 1446033709, 34 | "state": "completed" 35 | } 36 | ] 37 | } 38 | 39 | self.error_feed = { 40 | "app_id": "app_id", 41 | "job_id": "super_awesome_job", 42 | "pages": {}, 43 | "items": [] 44 | } 45 | 46 | @istest 47 | def it_gets_a_job(self): # noqa 48 | with patch.object(Client, 'get', return_value=self.job) as mock_method: # noqa 49 | self.client.jobs.find(id='super_awesome_job') 50 | mock_method.assert_called_once_with('/jobs/super_awesome_job', {}) 51 | 52 | @istest 53 | def it_gets_a_jobs_error_feed(self): # noqa 54 | with patch.object(Client, 'get', return_value=self.error_feed) as mock_method: # noqa 55 | self.client.jobs.errors(id='super_awesome_job') 56 | mock_method.assert_called_once_with('/jobs/super_awesome_job/error', {}) 57 | -------------------------------------------------------------------------------- /tests/unit/test_lead.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- # noqa 2 | 3 | import mock 4 | import unittest 5 | 6 | from intercom.collection_proxy import CollectionProxy 7 | from intercom.client import Client 8 | from intercom.lead import Lead 9 | from intercom.user import User 10 | from mock import patch 11 | from nose.tools import istest 12 | from tests.unit import get_user 13 | 14 | 15 | class LeadTest(unittest.TestCase): # noqa 16 | 17 | def setUp(self): # noqa 18 | self.client = Client() 19 | 20 | @istest 21 | def it_should_be_listable(self): # noqa 22 | proxy = self.client.leads.all() 23 | self.assertEquals('contacts', proxy.resource_name) 24 | self.assertEquals('/contacts', proxy.finder_url) 25 | self.assertEquals(Lead, proxy.resource_class) 26 | 27 | @istest 28 | def it_should_not_throw_errors_when_there_are_no_parameters(self): # noqa 29 | with patch.object(Client, 'post') as mock_method: # noqa 30 | self.client.leads.create() 31 | 32 | @istest 33 | def it_can_update_a_lead_with_an_id(self): # noqa 34 | lead = Lead(id="de45ae78gae1289cb") 35 | with patch.object(Client, 'put') as mock_method: # noqa 36 | self.client.leads.save(lead) 37 | mock_method.assert_called_once_with( 38 | '/contacts/de45ae78gae1289cb', {'custom_attributes': {}}) 39 | 40 | @istest 41 | def it_can_convert(self): # noqa 42 | lead = Lead.from_api({'user_id': 'contact_id'}) 43 | user = User.from_api({'id': 'user_id'}) 44 | 45 | with patch.object(Client, 'post', returns=get_user()) as mock_method: # noqa 46 | self.client.leads.convert(lead, user) 47 | mock_method.assert_called_once_with( 48 | '/contacts/convert', 49 | { 50 | 'contact': {'user_id': lead.user_id}, 51 | 'user': {'id': user.id} 52 | }) 53 | 54 | @istest 55 | def it_returns_a_collectionproxy_for_all_without_making_any_requests(self): # noqa 56 | with mock.patch('intercom.request.Request.send_request_to_path', new_callable=mock.NonCallableMock): # noqa 57 | res = self.client.leads.all() 58 | self.assertIsInstance(res, CollectionProxy) 59 | 60 | @istest 61 | def it_deletes_a_contact(self): # noqa 62 | lead = Lead(id="1") 63 | with patch.object(Client, 'delete') as mock_method: # noqa 64 | self.client.leads.delete(lead) 65 | mock_method.assert_called_once_with('/contacts/1', {}) 66 | -------------------------------------------------------------------------------- /tests/unit/test_message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import unittest 5 | 6 | from datetime import datetime 7 | from intercom.client import Client 8 | from intercom.user import User 9 | from mock import patch 10 | from nose.tools import eq_ 11 | from nose.tools import istest 12 | 13 | 14 | class MessageTest(unittest.TestCase): 15 | 16 | def setUp(self): # noqa 17 | self.client = Client() 18 | now = time.mktime(datetime.utcnow().timetuple()) 19 | self.user = User( 20 | email="jim@example.com", 21 | user_id="12345", 22 | created_at=now, 23 | name="Jim Bob") 24 | self.created_time = now - 300 25 | 26 | @istest 27 | def it_creates_a_user_message_with_string_keys(self): 28 | data = { 29 | 'from': { 30 | 'type': 'user', 31 | 'email': 'jim@example.com', 32 | }, 33 | 'body': 'halp' 34 | } 35 | with patch.object(Client, 'post', return_value=data) as mock_method: 36 | message = self.client.messages.create(**data) 37 | mock_method.assert_called_once_with('/messages/', data) 38 | eq_('halp', message.body) 39 | 40 | @istest 41 | def it_creates_an_admin_message(self): 42 | data = { 43 | 'from': { 44 | 'type': 'admin', 45 | 'id': '1234', 46 | }, 47 | 'to': { 48 | 'type': 'user', 49 | 'id': '5678', 50 | }, 51 | 'body': 'halp', 52 | 'message_type': 'inapp' 53 | } 54 | 55 | with patch.object(Client, 'post', return_value=data) as mock_method: 56 | message = self.client.messages.create(**data) 57 | mock_method.assert_called_once_with('/messages/', data) 58 | eq_('halp', message.body) 59 | eq_('inapp', message.message_type) 60 | -------------------------------------------------------------------------------- /tests/unit/test_note.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from intercom.client import Client 6 | from intercom.note import Note 7 | from mock import patch 8 | from nose.tools import eq_ 9 | from nose.tools import istest 10 | 11 | 12 | class NoteTest(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.client = Client() 16 | 17 | @istest 18 | def it_creates_a_note(self): 19 | data = { 20 | 'body': '

Note to leave on user

', 21 | 'created_at': 1234567890 22 | } 23 | with patch.object(Client, 'post', return_value=data) as mock_method: 24 | note = self.client.notes.create(body="Note to leave on user") 25 | mock_method.assert_called_once_with('/notes/', {'body': "Note to leave on user"}) # noqa 26 | eq_(note.body, "

Note to leave on user

") 27 | 28 | @istest 29 | def it_sets_gets_allowed_keys(self): 30 | params = { 31 | 'body': 'Note body', 32 | 'email': 'me@example.com', 33 | 'user_id': 'abc123' 34 | } 35 | params_keys = list(params.keys()) 36 | params_keys.sort() 37 | 38 | note = Note(**params) 39 | note_dict = note.to_dict() 40 | note_keys = list(note_dict.keys()) 41 | note_keys.sort() 42 | 43 | eq_(params_keys, note_keys) 44 | for key in params_keys: 45 | eq_(getattr(note, key), params[key]) 46 | -------------------------------------------------------------------------------- /tests/unit/test_notification.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from intercom.notification import Notification 6 | from intercom.utils import define_lightweight_class 7 | from nose.tools import eq_ 8 | from nose.tools import istest 9 | from tests.unit import test_conversation_notification 10 | from tests.unit import test_user_notification 11 | 12 | 13 | class NotificationTest(unittest.TestCase): 14 | 15 | @istest 16 | def it_converts_notification_hash_to_object(self): 17 | payload = Notification(**test_user_notification) 18 | self.assertIsInstance(payload, Notification) 19 | 20 | @istest 21 | def it_returns_correct_resource_type_for_part(self): 22 | payload = Notification(**test_user_notification) 23 | User = define_lightweight_class('user', 'User') # noqa 24 | 25 | self.assertIsInstance(payload.model.__class__, User.__class__) 26 | eq_(payload.model_type.__class__, User.__class__) 27 | 28 | @istest 29 | def it_returns_correct_user_notification_topic(self): 30 | payload = Notification(**test_user_notification) 31 | eq_(payload.topic, "user.created") 32 | 33 | @istest 34 | def it_returns_instance_of_user(self): 35 | User = define_lightweight_class('user', 'User') # noqa 36 | payload = Notification(**test_user_notification) 37 | self.assertIsInstance(payload.model.__class__, User.__class__) 38 | 39 | @istest 40 | def it_returns_instance_of_conversation(self): 41 | Conversation = define_lightweight_class('conversation', 'Conversation') # noqa 42 | payload = Notification(**test_conversation_notification) 43 | self.assertIsInstance(payload.model.__class__, Conversation.__class__) 44 | 45 | @istest 46 | def it_returns_correct_model_type_for_conversation(self): 47 | Conversation = define_lightweight_class('conversation', 'Conversation') # noqa 48 | payload = Notification(**test_conversation_notification) 49 | eq_(payload.model_type.__class__, Conversation.__class__) 50 | 51 | @istest 52 | def it_returns_correct_conversation_notification_topic(self): 53 | payload = Notification(**test_conversation_notification) 54 | eq_(payload.topic, "conversation.user.created") 55 | 56 | @istest 57 | def it_returns_inner_user_object_for_conversation(self): 58 | User = define_lightweight_class('user', 'User') # noqa 59 | payload = Notification(**test_conversation_notification) 60 | self.assertIsInstance(payload.model.user.__class__, User.__class__) 61 | 62 | @istest 63 | def it_returns_inner_conversation_parts_for_conversation(self): 64 | payload = Notification(**test_conversation_notification) 65 | conversation_parts = payload.data.item.conversation_parts 66 | eq_(1, len(conversation_parts)) 67 | eq_('conversation_part', conversation_parts[0].resource_type) 68 | 69 | @istest 70 | def it_returns_inner_user_object_with_nil_tags(self): 71 | user_notification = { 72 | "type": "notification_event", 73 | "app_id": "aa11aa", 74 | "data": { 75 | "type": "notification_event_data", 76 | "item": { 77 | "type": "user", 78 | "id": "abc123def", 79 | "user_id": "666", 80 | "email": "joe@example.com", 81 | "name": "Joe", 82 | "tags": { 83 | "type": "tag.list", 84 | "tags": None 85 | } 86 | } 87 | } 88 | } 89 | payload = Notification(**user_notification) 90 | eq_(payload.model.tags, []) 91 | 92 | @istest 93 | def it_has_self_attribute(self): 94 | payload = Notification(**test_conversation_notification) 95 | eq_('http://example.com/resource/url/', payload.self) 96 | -------------------------------------------------------------------------------- /tests/unit/test_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import intercom 4 | import json 5 | import unittest 6 | 7 | from intercom.client import Client 8 | from intercom.request import Request 9 | from intercom import UnexpectedError 10 | from mock import patch 11 | from nose.tools import assert_raises 12 | from nose.tools import eq_ 13 | from nose.tools import ok_ 14 | from nose.tools import istest 15 | from tests.unit import mock_response 16 | 17 | 18 | class RequestTest(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.client = Client() 22 | 23 | @istest 24 | def it_raises_resource_not_found(self): 25 | resp = mock_response(None, status_code=404) 26 | with patch('requests.request') as mock_method: 27 | mock_method.return_value = resp 28 | with assert_raises(intercom.ResourceNotFound): 29 | request = Request('GET', 'notes') 30 | request.send_request_to_path('', ('x', 'y'), resp) 31 | 32 | @istest 33 | def it_raises_authentication_error_unauthorized(self): 34 | resp = mock_response(None, status_code=401) 35 | with patch('requests.request') as mock_method: 36 | mock_method.return_value = resp 37 | with assert_raises(intercom.AuthenticationError): 38 | request = Request('GET', 'notes') 39 | request.send_request_to_path('', ('x', 'y'), resp) 40 | 41 | @istest 42 | def it_raises_authentication_error_forbidden(self): 43 | resp = mock_response(None, status_code=403) 44 | with patch('requests.request') as mock_method: 45 | mock_method.return_value = resp 46 | with assert_raises(intercom.AuthenticationError): 47 | request = Request('GET', 'notes') 48 | request.send_request_to_path('', ('x', 'y'), resp) 49 | 50 | @istest 51 | def it_raises_server_error(self): 52 | resp = mock_response(None, status_code=500) 53 | with patch('requests.request') as mock_method: 54 | mock_method.return_value = resp 55 | with assert_raises(intercom.ServerError): 56 | request = Request('GET', 'notes') 57 | request.send_request_to_path('', ('x', 'y'), resp) 58 | 59 | @istest 60 | def it_raises_bad_gateway_error(self): 61 | resp = mock_response(None, status_code=502) 62 | with patch('requests.request') as mock_method: 63 | mock_method.return_value = resp 64 | with assert_raises(intercom.BadGatewayError): 65 | request = Request('GET', 'notes') 66 | request.send_request_to_path('', ('x', 'y'), resp) 67 | 68 | @istest 69 | def it_raises_service_unavailable_error(self): 70 | resp = mock_response(None, status_code=503) 71 | with patch('requests.request') as mock_method: 72 | mock_method.return_value = resp 73 | with assert_raises(intercom.ServiceUnavailableError): 74 | request = Request('GET', 'notes') 75 | request.send_request_to_path('', ('x', 'y'), resp) 76 | 77 | @istest 78 | def it_raises_an_unexpected_typed_error(self): 79 | payload = { 80 | 'type': 'error.list', 81 | 'errors': [ 82 | { 83 | 'type': 'hopper', 84 | 'message': 'The first compiler.' 85 | } 86 | ] 87 | } 88 | content = json.dumps(payload).encode('utf-8') 89 | resp = mock_response(content) 90 | with patch('requests.sessions.Session.request') as mock_method: 91 | mock_method.return_value = resp 92 | try: 93 | self.client.get('/users', {}) 94 | self.fail('UnexpectedError not raised.') 95 | except (UnexpectedError) as err: 96 | ok_("The error of type 'hopper' is not recognized" in err.message) # noqa 97 | eq_(err.context['http_code'], 200) 98 | eq_(err.context['application_error_code'], 'hopper') 99 | 100 | @istest 101 | def it_raises_an_unexpected_untyped_error(self): 102 | payload = { 103 | 'type': 'error.list', 104 | 'errors': [ 105 | { 106 | 'message': 'UNIVAC' 107 | } 108 | ] 109 | } 110 | content = json.dumps(payload).encode('utf-8') 111 | resp = mock_response(content) 112 | with patch('requests.sessions.Session.request') as mock_method: 113 | mock_method.return_value = resp 114 | try: 115 | self.client.get('/users', {}) 116 | self.fail('UnexpectedError not raised.') 117 | except (UnexpectedError) as err: 118 | ok_("An unexpected error occured." in err.message) 119 | eq_(err.context['application_error_code'], None) 120 | 121 | @istest 122 | def it_raises_a_bad_request_error(self): 123 | payload = { 124 | 'type': 'error.list', 125 | 'errors': [ 126 | { 127 | 'type': None, 128 | 'message': 'email is required' 129 | } 130 | ] 131 | } 132 | 133 | for code in ['missing_parameter', 'parameter_invalid', 'bad_request']: 134 | payload['errors'][0]['type'] = code 135 | 136 | content = json.dumps(payload).encode('utf-8') 137 | resp = mock_response(content) 138 | with patch('requests.sessions.Session.request') as mock_method: 139 | mock_method.return_value = resp 140 | with assert_raises(intercom.BadRequestError): 141 | self.client.get('/users', {}) 142 | 143 | @istest 144 | def it_raises_an_authentication_error(self): 145 | payload = { 146 | 'type': 'error.list', 147 | 'errors': [ 148 | { 149 | 'type': 'unauthorized', 150 | 'message': 'Your name\'s not down.' 151 | } 152 | ] 153 | } 154 | for code in ['unauthorized', 'forbidden']: 155 | payload['errors'][0]['type'] = code 156 | 157 | content = json.dumps(payload).encode('utf-8') 158 | resp = mock_response(content) 159 | with patch('requests.sessions.Session.request') as mock_method: 160 | mock_method.return_value = resp 161 | with assert_raises(intercom.AuthenticationError): 162 | self.client.get('/users', {}) 163 | 164 | @istest 165 | def it_raises_resource_not_found_by_type(self): 166 | payload = { 167 | 'type': 'error.list', 168 | 'errors': [ 169 | { 170 | 'type': 'not_found', 171 | 'message': 'Waaaaally?' 172 | } 173 | ] 174 | } 175 | content = json.dumps(payload).encode('utf-8') 176 | resp = mock_response(content) 177 | with patch('requests.sessions.Session.request') as mock_method: 178 | mock_method.return_value = resp 179 | with assert_raises(intercom.ResourceNotFound): 180 | self.client.get('/users', {}) 181 | 182 | @istest 183 | def it_raises_rate_limit_exceeded(self): 184 | payload = { 185 | 'type': 'error.list', 186 | 'errors': [ 187 | { 188 | 'type': 'rate_limit_exceeded', 189 | 'message': 'Fair use please.' 190 | } 191 | ] 192 | } 193 | content = json.dumps(payload).encode('utf-8') 194 | resp = mock_response(content) 195 | with patch('requests.sessions.Session.request') as mock_method: 196 | mock_method.return_value = resp 197 | with assert_raises(intercom.RateLimitExceeded): 198 | self.client.get('/users', {}) 199 | 200 | @istest 201 | def it_raises_a_service_unavailable_error(self): 202 | payload = { 203 | 'type': 'error.list', 204 | 'errors': [ 205 | { 206 | 'type': 'service_unavailable', 207 | 'message': 'Zzzzz.' 208 | } 209 | ] 210 | } 211 | content = json.dumps(payload).encode('utf-8') 212 | resp = mock_response(content) 213 | with patch('requests.sessions.Session.request') as mock_method: 214 | mock_method.return_value = resp 215 | with assert_raises(intercom.ServiceUnavailableError): 216 | self.client.get('/users', {}) 217 | 218 | @istest 219 | def it_raises_a_multiple_matching_users_error(self): 220 | payload = { 221 | 'type': 'error.list', 222 | 'errors': [ 223 | { 224 | 'type': 'conflict', 225 | 'message': 'Two many cooks.' 226 | } 227 | ] 228 | } 229 | content = json.dumps(payload).encode('utf-8') 230 | resp = mock_response(content) 231 | with patch('requests.sessions.Session.request') as mock_method: 232 | mock_method.return_value = resp 233 | with assert_raises(intercom.MultipleMatchingUsersError): 234 | self.client.get('/users', {}) 235 | 236 | @istest 237 | def it_raises_token_unauthorized(self): 238 | payload = { 239 | 'type': 'error.list', 240 | 'errors': [ 241 | { 242 | 'type': 'token_unauthorized', 243 | 'message': 'The PAT is not authorized for this action.' 244 | } 245 | ] 246 | } 247 | content = json.dumps(payload).encode('utf-8') 248 | resp = mock_response(content) 249 | with patch('requests.sessions.Session.request') as mock_method: 250 | mock_method.return_value = resp 251 | with assert_raises(intercom.TokenUnauthorizedError): 252 | self.client.get('/users', {}) 253 | 254 | @istest 255 | def it_handles_no_error_type(self): 256 | payload = { 257 | 'errors': [ 258 | { 259 | 'code': 'unique_user_constraint', 260 | 'message': 'User already exists.' 261 | } 262 | ], 263 | 'request_id': '00000000-0000-0000-0000-000000000000', 264 | 'type': 'error.list' 265 | } 266 | content = json.dumps(payload).encode('utf-8') 267 | resp = mock_response(content) 268 | with patch('requests.sessions.Session.request') as mock_method: 269 | mock_method.return_value = resp 270 | with assert_raises(intercom.MultipleMatchingUsersError): 271 | self.client.get('/users', {}) 272 | 273 | payload = { 274 | 'errors': [ 275 | { 276 | 'code': 'parameter_not_found', 277 | 'message': 'missing data parameter' 278 | } 279 | ], 280 | 'request_id': None, 281 | 'type': 'error.list' 282 | } 283 | content = json.dumps(payload).encode('utf-8') 284 | resp = mock_response(content) 285 | with patch('requests.sessions.Session.request') as mock_method: 286 | mock_method.return_value = resp 287 | with assert_raises(intercom.BadRequestError): 288 | self.client.get('/users', {}) 289 | 290 | @istest 291 | def it_handles_empty_responses(self): 292 | resp = mock_response('', status_code=202) 293 | with patch('requests.request') as mock_method: 294 | mock_method.return_value = resp 295 | request = Request('GET', 'events') 296 | request.send_request_to_path('', ('x', 'y'), resp) 297 | 298 | resp = mock_response(' ', status_code=202) 299 | with patch('requests.request') as mock_method: 300 | mock_method.return_value = resp 301 | request = Request('GET', 'events') 302 | request.send_request_to_path('', ('x', 'y'), resp) 303 | 304 | @istest 305 | def it_handles_no_encoding(self): 306 | resp = mock_response( 307 | ' ', status_code=200, encoding=None, headers=None) 308 | resp.apparent_encoding = 'utf-8' 309 | 310 | with patch('requests.request') as mock_method: 311 | mock_method.return_value = resp 312 | request = Request('GET', 'events') 313 | request.send_request_to_path('', ('x', 'y'), resp) 314 | 315 | @istest 316 | def it_needs_encoding_or_apparent_encoding(self): 317 | payload = '{}' 318 | 319 | if not hasattr(payload, 'decode'): 320 | # python 3 321 | payload = payload.encode('utf-8') 322 | 323 | resp = mock_response( 324 | payload, status_code=200, encoding=None, headers=None) 325 | 326 | with patch('requests.request') as mock_method: 327 | mock_method.return_value = resp 328 | with assert_raises(TypeError): 329 | request = Request('GET', 'events') 330 | request.send_request_to_path('', ('x', 'y'), resp) 331 | 332 | @istest 333 | def it_allows_the_timeout_to_be_changed(self): 334 | from intercom.request import Request 335 | try: 336 | eq_(90, Request.timeout) 337 | Request.timeout = 3 338 | eq_(3, Request.timeout) 339 | finally: 340 | Request.timeout = 90 341 | 342 | @istest 343 | def it_allows_the_timeout_to_be_configured(self): 344 | import os 345 | from intercom.request import configure_timeout 346 | 347 | # check the default 348 | eq_(90, configure_timeout()) 349 | 350 | # override the default 351 | os.environ['INTERCOM_REQUEST_TIMEOUT'] = '20' 352 | eq_(20, configure_timeout()) 353 | 354 | # ignore bad timeouts, reset to default 90 355 | os.environ['INTERCOM_REQUEST_TIMEOUT'] = 'abc' 356 | eq_(90, configure_timeout()) 357 | -------------------------------------------------------------------------------- /tests/unit/test_scroll_collection_proxy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Test module for Scroll Collection Proxy.""" 3 | import unittest 4 | 5 | from intercom import HttpError 6 | from intercom.client import Client 7 | from mock import call 8 | from mock import patch 9 | from nose.tools import assert_raises 10 | from nose.tools import eq_ 11 | from nose.tools import istest 12 | from tests.unit import users_scroll 13 | 14 | 15 | class CollectionProxyTest(unittest.TestCase): # noqa 16 | 17 | def setUp(self): # noqa 18 | self.client = Client() 19 | 20 | @istest 21 | def it_stops_iterating_if_no_users_returned(self): # noqa 22 | body = users_scroll(include_users=False) 23 | with patch.object(Client, 'get', return_value=body) as mock_method: 24 | emails = [user.email for user in self.client.users.scroll()] 25 | mock_method.assert_called('/users/scroll', {}) 26 | eq_(emails, []) # noqa 27 | 28 | @istest 29 | def it_keeps_iterating_if_users_returned(self): # noqa 30 | page1 = users_scroll(include_users=True) 31 | page2 = users_scroll(include_users=False) 32 | side_effect = [page1, page2] 33 | with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa 34 | emails = [user.email for user in self.client.users.scroll()] 35 | eq_([call('/users/scroll', {}), call('/users/scroll', {'scroll_param': 'da6bbbac-25f6-4f07-866b-b911082d7'})], # noqa 36 | mock_method.mock_calls) 37 | eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com']) # noqa 38 | 39 | @istest 40 | def it_supports_indexed_array_access(self): # noqa 41 | body = users_scroll(include_users=True) 42 | with patch.object(Client, 'get', return_value=body) as mock_method: 43 | eq_(self.client.users.scroll()[0].email, 'user1@example.com') 44 | mock_method.assert_called_once_with('/users/scroll', {}) 45 | eq_(self.client.users.scroll()[1].email, 'user2@example.com') 46 | 47 | @istest 48 | def it_returns_one_page_scroll(self): # noqa 49 | body = users_scroll(include_users=True) 50 | with patch.object(Client, 'get', return_value=body): 51 | scroll = self.client.users.scroll() 52 | scroll.get_next_page() 53 | emails = [user['email'] for user in scroll.resources] 54 | eq_(emails, ['user1@example.com', 'user2@example.com', 'user3@example.com']) # noqa 55 | 56 | @istest 57 | def it_keeps_iterating_if_called_with_scroll_param(self): # noqa 58 | page1 = users_scroll(include_users=True) 59 | page2 = users_scroll(include_users=False) 60 | side_effect = [page1, page2] 61 | with patch.object(Client, 'get', side_effect=side_effect) as mock_method: # noqa 62 | scroll = self.client.users.scroll() 63 | scroll.get_page() 64 | scroll.get_page('da6bbbac-25f6-4f07-866b-b911082d7') 65 | emails = [user['email'] for user in scroll.resources] 66 | eq_(emails, []) # noqa 67 | 68 | @istest 69 | def it_works_with_an_empty_list(self): # noqa 70 | body = users_scroll(include_users=False) 71 | with patch.object(Client, 'get', return_value=body) as mock_method: # noqa 72 | scroll = self.client.users.scroll() 73 | scroll.get_page() 74 | emails = [user['email'] for user in scroll.resources] 75 | eq_(emails, []) # noqa 76 | 77 | @istest 78 | def it_raises_an_http_error(self): # noqa 79 | with patch.object(Client, 'get', return_value=None) as mock_method: # noqa 80 | scroll = self.client.users.scroll() 81 | with assert_raises(HttpError): 82 | scroll.get_page() 83 | -------------------------------------------------------------------------------- /tests/unit/test_subscription.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from intercom.client import Client 6 | from mock import patch 7 | from nose.tools import eq_ 8 | from nose.tools import istest 9 | from tests.unit import test_subscription 10 | 11 | 12 | class SubscriptionTest(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.client = Client() 16 | 17 | @istest 18 | def it_gets_a_subscription(self): 19 | with patch.object(Client, 'get', return_value=test_subscription) as mock_method: # noqa 20 | subscription = self.client.subscriptions.find(id="nsub_123456789") 21 | eq_(subscription.topics[0], "user.created") 22 | eq_(subscription.topics[1], "conversation.user.replied") 23 | eq_(subscription.self, 24 | "https://api.intercom.io/subscriptions/nsub_123456789") 25 | mock_method.assert_called_once_with('/subscriptions/nsub_123456789', {}) # noqa 26 | 27 | @istest 28 | def it_creates_a_subscription(self): 29 | with patch.object(Client, 'post', return_value=test_subscription) as mock_method: # noqa 30 | subscription = self.client.subscriptions.create( 31 | url="http://example.com", 32 | topics=["user.created"] 33 | ) 34 | eq_(subscription.topics[0], "user.created") 35 | eq_(subscription.url, "http://example.com") 36 | mock_method.assert_called_once_with( 37 | '/subscriptions/', {'url': "http://example.com", 'topics': ["user.created"]}) # noqa 38 | -------------------------------------------------------------------------------- /tests/unit/test_tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from intercom.client import Client 6 | from mock import patch 7 | from nose.tools import eq_ 8 | from nose.tools import istest 9 | from tests.unit import test_tag 10 | 11 | 12 | class TagTest(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.client = Client() 16 | 17 | @istest 18 | def it_gets_a_tag(self): 19 | with patch.object(Client, 'get', return_value=test_tag) as mock_method: # noqa 20 | tag = self.client.tags.find(name="Test Tag") 21 | eq_(tag.name, "Test Tag") 22 | mock_method.assert_called_once_with('/tags', {'name': "Test Tag"}) 23 | 24 | @istest 25 | def it_creates_a_tag(self): 26 | with patch.object(Client, 'post', return_value=test_tag) as mock_method: # noqa 27 | tag = self.client.tags.create(name="Test Tag") 28 | eq_(tag.name, "Test Tag") 29 | mock_method.assert_called_once_with('/tags/', {'name': "Test Tag"}) 30 | 31 | @istest 32 | def it_tags_users(self): 33 | params = { 34 | 'name': 'Test Tag', 35 | 'user_ids': ['abc123', 'def456'], 36 | 'tag_or_untag': 'tag' 37 | } 38 | with patch.object(Client, 'post', return_value=test_tag) as mock_method: # noqa 39 | tag = self.client.tags.create(**params) 40 | eq_(tag.name, "Test Tag") 41 | eq_(tag.tagged_user_count, 2) 42 | mock_method.assert_called_once_with('/tags/', params) 43 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Unit test module for utils.py.""" 3 | import unittest 4 | 5 | from intercom.utils import define_lightweight_class 6 | from nose.tools import eq_ 7 | from nose.tools import istest 8 | 9 | 10 | class UserTest(unittest.TestCase): # noqa 11 | 12 | @istest 13 | def it_has_a_resource_type(self): # noqa 14 | Avatar = define_lightweight_class('avatar', 'Avatar') # noqa 15 | eq_('avatar', Avatar.resource_type) 16 | avatar = Avatar() 17 | eq_('avatar', avatar.resource_type) 18 | -------------------------------------------------------------------------------- /tests/unit/traits/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/unit/traits/test_api_resource.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | from datetime import datetime 6 | from intercom.traits.api_resource import Resource 7 | from nose.tools import assert_raises 8 | from nose.tools import eq_ 9 | from nose.tools import ok_ 10 | from nose.tools import istest 11 | from pytz import utc 12 | 13 | 14 | class IntercomTraitsApiResource(unittest.TestCase): 15 | 16 | def setUp(self): # noqa 17 | self.object_json = { 18 | "type": "company", 19 | "id": "aaaaaaaaaaaaaaaaaaaaaaaa", 20 | "app_id": "some-app-id", 21 | "name": "SuperSuite", 22 | "plan_id": 1, 23 | "remote_company_id": "8", 24 | "remote_created_at": 103201, 25 | "created_at": 1374056196, 26 | "user_count": 1, 27 | "custom_attributes": {} 28 | } 29 | self.api_resource = Resource().from_response(self.object_json) 30 | self.api_resource_obj = super(Resource, self.api_resource) 31 | 32 | @istest 33 | def it_does_not_set_type_on_parsing_json(self): 34 | ok_(not hasattr(self.api_resource, 'type')) 35 | 36 | @istest 37 | def it_coerces_time_on_parsing_json(self): 38 | dt = datetime.utcfromtimestamp(1374056196).replace(tzinfo=utc) 39 | eq_(dt, self.api_resource.created_at) 40 | 41 | @istest 42 | def it_dynamically_defines_accessors_for_non_existent_properties(self): 43 | ok_(not hasattr(self.api_resource, 'spiders')) 44 | self.api_resource.spiders = 4 45 | ok_(hasattr(self.api_resource, 'spiders')) 46 | 47 | @istest 48 | def it_calls_dynamically_defined_getter_when_asked(self): 49 | self.api_resource.foo = 4 50 | eq_(4, self.api_resource.foo) 51 | 52 | @istest 53 | def it_accepts_unix_timestamps_into_dynamically_defined_date_setters(self): 54 | self.api_resource.foo_at = 1401200468 55 | eq_(1401200468, self.api_resource_obj.__getattribute__('foo_at')) 56 | 57 | @istest 58 | def it_exposes_dates_correctly_for_dynamically_defined_getters(self): 59 | self.api_resource.foo_at = 1401200468 60 | dt = datetime.utcfromtimestamp(1401200468).replace(tzinfo=utc) 61 | eq_(dt, self.api_resource.foo_at) 62 | 63 | @istest 64 | def it_throws_regular_error_when_non_existant_getter_is_called_that_is_backed_by_an_instance_variable(self): # noqa 65 | super(Resource, self.api_resource).__setattr__('bar', 'you cant see me') # noqa 66 | self.api_resource.bar 67 | 68 | @istest 69 | def it_throws_attribute_error_when_non_existent_attribute_is_called(self): 70 | with assert_raises(AttributeError): 71 | self.api_resource.flubber 72 | 73 | @istest 74 | def it_throws_attribute_error_when_non_existent_method_is_called(self): 75 | with assert_raises(AttributeError): 76 | self.api_resource.flubber() 77 | 78 | @istest 79 | def it_throws_attribute_error_when_non_existent_setter_is_called(self): 80 | with assert_raises(AttributeError): 81 | self.api_resource.flubber('a', 'b') 82 | 83 | @istest 84 | def it_create_an_initialized_resource_equal_to_a_from_response_resource(self): # noqa 85 | initialized_api_resource = Resource(**self.object_json) 86 | for key in list(self.object_json.keys()): 87 | if key == "type": 88 | continue 89 | eq_(getattr(initialized_api_resource, key), getattr(self.api_resource, key)) # noqa 90 | --------------------------------------------------------------------------------