├── README
├── gnippy
├── test
│ ├── __init__.py
│ ├── test_config.py
│ ├── test_utils.py
│ ├── test_powertrackclient.py
│ └── test_rules.py
├── __init__.py
├── errors.py
├── powertrackclient.py
├── config.py
└── rules.py
├── MANIFEST.in
├── requirements-dev.txt
├── Makefile
├── .gitignore
├── tox.ini
├── .travis.yml
├── LICENSE.txt
├── CHANGES.txt
├── setup.py
├── README.rst
└── CLA
├── j-bennet.txt
├── mu.txt
└── cla_template.txt
/README:
--------------------------------------------------------------------------------
1 | README.rst
--------------------------------------------------------------------------------
/gnippy/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.txt
2 | recursive-include docs *.txt
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | mock==1.0.1
2 | nose==1.3.0
3 | tox>=1.9.2
4 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 |
3 | clean:
4 | rm -Rf build
5 |
6 | publish:
7 | python setup.py sdist upload
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /MANIFEST
2 | /dist/*
3 | *.pyc
4 | .idea*
5 | gnippy.egg-info*
6 | .python-version
7 | /.tox
8 | build/
9 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py26, py27, py33, py35
3 | [testenv]
4 | deps = py26: unittest2
5 | nose
6 | mock
7 | requests==2.8.1
8 | commands = nosetests
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | python:
4 | - "2.7"
5 | - "3.4"
6 | - "3.5"
7 | - "3.6"
8 |
9 | install:
10 | - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install unittest2; fi
11 | - pip install -r requirements-dev.txt
12 | - pip install -e .
13 |
14 | script:
15 | - nosetests
16 |
--------------------------------------------------------------------------------
/gnippy/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # gnippy - GNIP for Python
3 | from __future__ import absolute_import
4 |
5 | __title__ = 'gnippy'
6 | __version__ = '0.4.1'
7 | __author__ = 'Abhinav Ajgaonkar'
8 | __license__ = 'Apache 2.0'
9 | __copyright__ = 'Copyright 2012-2015 Abhinav Ajgaonkar'
10 |
11 | from .powertrackclient import PowerTrackClient
12 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2012-2016 Abhinav Ajgaonkar and the Gnippy contributors.
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/CHANGES.txt:
--------------------------------------------------------------------------------
1 | v0.1.0, 2012-12-06 -- Initial release.
2 | v0.1.1, 2012-12-06 -- Bugfix in constructor.
3 | v0.1.2, 2012-12-06 -- Adding background worker to prevent blocking while data is being fetched.
4 | v0.1.3, 2013-05-14 -- Upgrading to requests v1.2.0.
5 | v0.1.4 2013-05-15 -- Fixing broken pip install.
6 | v0.2.0 2013-05-15 -- Adding support for file based configuration. Adding unit tests.
7 | v0.3.0 2013-05-19 -- Adding initial support for the Rules API and tests.
8 | v0.3.1 2013-05-19 -- Bugfixes for the rules API URL generation.
9 | v0.3.2 2013-05-19 -- Renaming build_rule() to just build().
10 | v0.3.3 2013-05-20 -- Adding the ability to list all current rules + unit tests
11 | v0.3.4 2013-05-20 -- Adding the ability to delete rules + unit tests
12 | --
13 |
14 | This file is no longer maintained. See "releases" on GitHub for a list of changes.
15 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 | version = "0.7.0"
7 |
8 | try:
9 | from setuptools import setup
10 | except ImportError:
11 | from distutils.core import setup
12 |
13 | if sys.argv[-1] == "publish":
14 | os.system("python setup.py sdist register upload")
15 | sys.exit(1)
16 |
17 | # These are required because sometimes PyPI refuses to bundle certain files
18 | try:
19 | long_desc = open('README').read()
20 | except:
21 | long_desc = ""
22 |
23 | try:
24 | license = open('LICENSE.txt').read()
25 | except:
26 | license = "Apache 2.0 License"
27 |
28 | setup(
29 | name='gnippy',
30 | version=version,
31 | description='Python library for GNIP.',
32 | long_description=long_desc,
33 | author='Abhinav Ajgaonkar',
34 | author_email='abhinav316@gmail.com',
35 | packages=['gnippy'],
36 | url='http://pypi.python.org/pypi/gnippy/',
37 | license=license,
38 | install_requires=[
39 | "requests>=2.8.1,<3.0.0",
40 | "six>=1.10.0"
41 | ]
42 | )
43 |
--------------------------------------------------------------------------------
/gnippy/errors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | class ConfigFileNotFoundException(Exception):
4 | """ Raised when an invalid config_file_path argument was passed. """
5 | pass
6 |
7 |
8 | class IncompleteConfigurationException(Exception):
9 | """ Raised when no .gnippy file is found. """
10 | pass
11 |
12 |
13 | class BadArgumentException(Exception):
14 | """ Raised when an invalid argument is detected. """
15 | pass
16 |
17 |
18 | class RuleAddFailedException(Exception):
19 | """ Raised when a rule add fails. """
20 | pass
21 |
22 |
23 | class RulesListFormatException(Exception):
24 | """ Raised when rules_list is not in the correct format. """
25 | pass
26 |
27 |
28 | class RulesGetFailedException(Exception):
29 | """ Raised when listing the current rule set fails. """
30 | pass
31 |
32 |
33 | class BadPowerTrackUrlException(Exception):
34 | """ Raised when the PowerTrack URL looks like its incorrect. """
35 | pass
36 |
37 |
38 | class RuleDeleteFailedException(Exception):
39 | """ Raised when a rule delete fails. """
40 | pass
--------------------------------------------------------------------------------
/gnippy/test/test_config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | try:
3 | import unittest2 as unittest
4 | except ImportError:
5 | import unittest
6 |
7 | from gnippy import config as gnippy_config
8 | from gnippy.test import test_utils
9 |
10 |
11 | class ConfigTestCase(unittest.TestCase):
12 |
13 | def setUp(self):
14 | test_utils.unset_environment_config_vars()
15 |
16 | def tearDown(self):
17 | """ Delete the test config file at test_config_path """
18 | test_utils.delete_test_config()
19 | test_utils.unset_environment_config_vars()
20 |
21 | def test_default_path(self):
22 | """
23 | Infer the user's home directory (either in /Users/.. or /home/..)
24 | and check if the default path produces the corrrect output.
25 | """
26 | possible_paths = test_utils.get_possible_config_locations()
27 | actual = gnippy_config.get_default_config_file_path()
28 | self.assertTrue(actual in possible_paths)
29 |
30 | def test_config_parsing_full(self):
31 | """ Read the test config and compare values. """
32 | test_utils.generate_test_config_file()
33 | result = gnippy_config.get_config(test_utils.test_config_path)
34 | self.assertEqual(result['Credentials']['username'], test_utils.test_username)
35 | self.assertEqual(result['Credentials']['password'], test_utils.test_password)
36 | self.assertEqual(result['PowerTrack']['url'], test_utils.test_powertrack_url)
37 | self.assertEqual(result['PowerTrack']['rules_url'], test_utils.test_rules_url)
38 |
39 | def test_config_parsing_halt(self):
40 | """ Read the half config file and compare values. """
41 | test_utils.generate_test_config_file_with_only_auth()
42 | result = gnippy_config.get_config(test_utils.test_config_path)
43 | self.assertEqual(result['Credentials']['username'], test_utils.test_username)
44 | self.assertEqual(result['Credentials']['password'], test_utils.test_password)
45 | self.assertEqual(result['PowerTrack']['url'], None)
46 | self.assertEqual(result['PowerTrack']['rules_url'], None)
47 |
48 | def test_resolve_file_arg(self):
49 | """ Run the "resolve" method with just a filename and check if all info is loaded. """
50 | test_utils.generate_test_config_file()
51 | conf = gnippy_config.resolve({"config_file_path": test_utils.test_config_path})
52 | self.assertEqual(conf['auth'][0], test_utils.test_username)
53 | self.assertEqual(conf['auth'][1], test_utils.test_password)
54 | self.assertEqual(conf['url'], test_utils.test_powertrack_url)
55 | self.assertEqual(conf['rules_url'], test_utils.test_rules_url)
56 |
57 | def test_resolve_conf_from_environment_variables(self):
58 | """ Run the "resolve" method providing env vars and check if all info is loaded. """
59 | test_utils.delete_test_config()
60 | test_utils.set_environment_config_vars()
61 | conf = gnippy_config.resolve({})
62 | self.assertEqual(conf['url'], test_utils.test_powertrack_url)
63 | self.assertEqual(conf['rules_url'], test_utils.test_rules_url)
64 | self.assertEqual(conf['auth'][0], test_utils.test_username)
65 | self.assertEqual(conf['auth'][1], test_utils.test_password)
66 |
67 | def test_resolve_conf_from_kwargs1(self):
68 | """ Run the "resolve" method providing kwargs and check if all info is loaded. """
69 | test_utils.delete_test_config()
70 | rules_url = 'http://gnip.rules.url'
71 | username = 'gnip-username'
72 | password = 'gnip-password'
73 | conf = gnippy_config.resolve({
74 | 'rules_url': rules_url,
75 | 'auth': (username, password)
76 | })
77 | self.assertEqual(conf['rules_url'], rules_url)
78 | self.assertEqual(conf['auth'][0], username)
79 | self.assertEqual(conf['auth'][1], password)
80 |
81 | def test_resolve_conf_from_kwargs2(self):
82 | """ Run the "resolve" method providing kwargs and check if all info is loaded. """
83 | test_utils.delete_test_config()
84 | url = 'http://gnip.url'
85 | username = 'gnip-username'
86 | password = 'gnip-password'
87 | conf = gnippy_config.resolve({
88 | 'url': url,
89 | 'auth': (username, password)
90 | })
91 | self.assertEqual(conf['url'], url)
92 | self.assertEqual(conf['auth'][0], username)
93 | self.assertEqual(conf['auth'][1], password)
94 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | gnippy: Python library for GNIP
2 | ===============================
3 |
4 | .. image:: https://badge.fury.io/py/gnippy.svg
5 | :target: https://pypi.python.org/pypi/gnippy
6 |
7 | .. image:: https://img.shields.io/pypi/dm/gnippy.svg
8 | :target: https://pypi.python.org/pypi/gnippy
9 |
10 | .. image:: https://travis-ci.org/abh1nav/gnippy.svg?branch=master
11 | :target: https://travis-ci.org/abh1nav/gnippy
12 |
13 | gnippy provides an easy way to access the `Power Track `_ stream provided by GNIP.
14 | You can also use gnippy to programatically add rules to your Power Track stream.
15 |
16 | Install
17 | -------
18 | .. code-block:: python
19 |
20 | pip install gnippy
21 |
22 | Quickstart
23 | ----------
24 | Create a .gnippy file and place it in your home directory. It should contain the following:
25 |
26 | .. code-block:: text
27 |
28 | [Credentials]
29 | username = user@company.com
30 | password = mypassword
31 |
32 | [PowerTrack]
33 | url = https://my.gnip.powertrack/url.json
34 | rules_url = https://api.gnip.powertrack/rules.json
35 |
36 | Fire up the client:
37 |
38 | .. code-block:: python
39 |
40 | #!/usr/bin/env python
41 | import time
42 | from gnippy import PowerTrackClient
43 |
44 | # Define a callback
45 | def callback(activity):
46 | print activity
47 |
48 | # Create the client
49 | client = PowerTrackClient(callback)
50 | client.connect()
51 |
52 | # Wait for 2 minutes and then disconnect
53 | time.sleep(120)
54 | client.disconnect()
55 |
56 | Configuration
57 | -------------
58 |
59 | If you don't want to create a config file or you want it put it in another location:
60 |
61 | .. code-block:: python
62 |
63 | client = PowerTrackClient(callback, config_file_path="/etc/gnippy")
64 | # OR ... provide the url and authentication credentials to override any config files
65 | client = PowerTrackClient(callback, url="http://my.gnip.powertrack/url.json", auth=("uname", "pwd"))
66 |
67 | You can also configure gnippy using environment variables:
68 |
69 | .. code-block:: text
70 |
71 | GNIPPY_URL
72 | GNIPPY_RULES_URL
73 | GNIPPY_AUTH_USERNAME
74 | GNIPPY_AUTH_PASSWORD
75 |
76 |
77 |
78 |
79 |
80 | Adding PowerTrack Rules
81 | -----------------------
82 |
83 | If you want to add `rules `_ to your PowerTrack:
84 |
85 | .. code-block:: python
86 |
87 | from gnippy import rules
88 | from gnippy.errors import RuleAddFailedException
89 |
90 | # Synchronously add rules
91 | try:
92 | rules.add_rule('(Hello OR World OR "this is a test") lang:en', tag="MyRule")
93 | rules.add_rule('Rule without a tag')
94 | except RuleAddFailedException:
95 | pass
96 |
97 | # OR ... synchronously add multiple rules at once
98 | rule_list = []
99 | rule_list.append(rules.build("Hello World", tag="asdf"))
100 | rule_list.append(rules.build("Rule Without a Tag"))
101 | try:
102 | rules.add_rules(rule_list)
103 | except RuleAddFailedException:
104 | pass
105 |
106 | # OR ... manually pass in params - overrides any config files
107 | rules.add_rule("My Rule String", tag="mytag", rules_url="https://api.gnip.powertrack/rules.json", \
108 | auth=("uname", "pwd"))
109 |
110 |
111 | Listing Active PowerTrack Rules
112 | -------------------------------
113 |
114 | .. code-block:: python
115 |
116 | from gnippy import rules
117 | from gnippy.errors import RulesGetFailedException
118 |
119 | try:
120 | rules_list = rules.get_rules()
121 | # rules_list is in the format:
122 | # [
123 | # { "value": "(Hello OR World) AND lang:en" },
124 | # { "value": "Hello", "tag": "mytag" }
125 | # ]
126 | except RulesGetFailedException:
127 | pass
128 |
129 | Deleting PowerTrack Rules
130 | -------------------------
131 |
132 | .. code-block:: python
133 |
134 | from gnippy import rules
135 | from gnippy.errors import RuleDeleteFailedException, RulesGetFailedException
136 |
137 | try:
138 | rules_list = rules.get_rules()
139 | # Suppose I want to delete the first rule in the list
140 | rules.delete_rule(rules_list[0])
141 | # OR ... I want to delete ALL rules
142 | rules.delete_rules(rules_list)
143 |
144 | except RuleDeleteFailedException, RulesGetFailedException:
145 | pass
146 |
147 | Source available on GitHub: http://github.com/abh1nav/gnippy/
148 |
--------------------------------------------------------------------------------
/gnippy/powertrackclient.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from contextlib import closing
4 | import sys
5 | import threading
6 |
7 | try:
8 | import urlparse
9 | from urllib import urlencode
10 | except: # For Python 3
11 | import urllib.parse as urlparse
12 | from urllib.parse import urlencode
13 |
14 | import requests
15 |
16 | from gnippy import config
17 |
18 |
19 | def append_backfill_to_url(url, backfill_minutes):
20 | parsed = list(urlparse.urlparse(url))
21 |
22 | params = {'backfillMinutes': backfill_minutes}
23 |
24 | qs = dict(urlparse.parse_qsl(parsed[4]))
25 |
26 | qs.update(params)
27 |
28 | parsed[4] = urlencode(qs)
29 |
30 | return urlparse.urlunparse(parsed)
31 |
32 |
33 | class PowerTrackClient:
34 | """
35 | PowerTrackClient allows you to connect to the GNIP
36 | power track stream and fetch data.
37 | """
38 | callback = None
39 | url = None
40 | auth = None
41 |
42 | def __init__(self, callback, exception_callback=None, **kwargs):
43 | self.callback = callback
44 | self.exception_callback = exception_callback
45 | c = config.resolve(kwargs)
46 | self.url = c['url']
47 | self.auth = c['auth']
48 |
49 | def connect(self, backfill_minutes=None):
50 |
51 | connection_url = self.get_connection_url(backfill_minutes)
52 |
53 | self.worker = Worker(
54 | url=connection_url,
55 | auth=self.auth,
56 | callback=self.callback,
57 | exception_callback=self.exception_callback)
58 |
59 | self.worker.setDaemon(True)
60 |
61 | self.worker.start()
62 |
63 | def get_connection_url(self, backfill_minutes=None):
64 | connection_url = self.url
65 |
66 | if backfill_minutes:
67 | assert type(backfill_minutes) is int, \
68 | "backfill_minutes is not an integer: {0}".format(
69 | backfill_minutes)
70 |
71 | assert backfill_minutes <= 5, \
72 | "backfill_minutes should be 5 or less: {0}".format(
73 | backfill_minutes)
74 |
75 | connection_url = append_backfill_to_url(
76 | connection_url, backfill_minutes)
77 |
78 | return connection_url
79 |
80 | def connected(self):
81 |
82 | return not self.worker.stopped()
83 |
84 | def disconnect(self):
85 | self.worker.stop()
86 | self.worker.join()
87 |
88 | def load_config_from_file(self, url, auth, config_file_path):
89 | """ Attempt to load the config from a file. """
90 | conf = config.get_config(config_file_path=config_file_path)
91 |
92 | if url is None:
93 | conf_url = conf['PowerTrack']['url']
94 | if conf_url:
95 | self.url = conf['PowerTrack']['url']
96 | else:
97 | self.url = url
98 |
99 | if auth is None:
100 | conf_uname = conf['Credentials']['username']
101 | conf_pwd = conf['Credentials']['password']
102 | self.auth = (conf_uname, conf_pwd)
103 | else:
104 | self.auth = auth
105 |
106 |
107 | class Worker(threading.Thread):
108 | """ Background worker to fetch data without blocking """
109 |
110 | def __init__(self, url, auth, callback, exception_callback=None):
111 | super(Worker, self).__init__()
112 | self.url = url
113 | self.auth = auth
114 | self.on_data = callback
115 | self.on_error = exception_callback
116 | self._stop_event = threading.Event()
117 |
118 | def stop(self):
119 | self._stop_event.set()
120 |
121 | def stopped(self):
122 | return self._stop_event.isSet()
123 |
124 | def run(self):
125 | try:
126 | with closing(
127 | requests.get(self.url, auth=self.auth, stream=True)) as r:
128 | if r.status_code != 200:
129 | self.stop()
130 | raise Exception(
131 | "GNIP returned HTTP {}".format(r.status_code))
132 |
133 | for line in r.iter_lines():
134 | if line:
135 | self.on_data(line)
136 |
137 | if self.stopped():
138 | break
139 | except Exception:
140 | if self.on_error:
141 | exinfo = sys.exc_info()
142 | self.on_error(exinfo)
143 | else:
144 | # re-raise the last exception as-is
145 | raise
146 | finally:
147 | if not self.stopped():
148 | # clean up and set the stop event
149 | self.stop()
150 |
--------------------------------------------------------------------------------
/gnippy/test/test_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | try:
4 | import ConfigParser
5 | except ImportError:
6 | import configparser as ConfigParser
7 | import os
8 | import pwd
9 | from six import PY2
10 |
11 | test_config_path = "/tmp/.gnippy"
12 | test_username = "TestUserName"
13 | test_password = "testP@ssw0rd"
14 | test_powertrack_url = "https://stream.gnip.com:443/accounts/Organization/publishers/twitter/streams/track/Production.json"
15 | test_rules_url = "https://api.gnip.com:443/accounts/Organization/publishers/twitter/streams/track/Production/rules.json"
16 |
17 |
18 | def _add_credentials(parser):
19 | """ Add the Credentials section to a ConfigParser. """
20 | parser.add_section(u"Credentials")
21 | parser.set(u"Credentials", u"username", test_username)
22 | parser.set(u"Credentials", u"password", test_password)
23 |
24 |
25 | def _add_power_track_urls(parser):
26 | """ Add the PowerTrack section to a ConfigParser. """
27 | parser.add_section("PowerTrack")
28 | parser.set("PowerTrack", "url", test_powertrack_url)
29 | parser.set("PowerTrack", "rules_url", test_rules_url)
30 |
31 |
32 | def _write_config_file(parser):
33 | """ Write out the contents of the provided ConfigParser to the test_config_path. """
34 | if PY2:
35 | with open(test_config_path, 'wb') as configfile:
36 | parser.write(configfile)
37 | else:
38 | import io
39 | with io.open(test_config_path, 'w', encoding='utf-8') as configfile:
40 | parser.write(configfile)
41 |
42 |
43 | def set_environment_config_vars():
44 | """ Set GNIPPY env variables. """
45 | os.environ["GNIPPY_URL"] = test_powertrack_url
46 | os.environ["GNIPPY_RULES_URL"] = test_rules_url
47 | os.environ["GNIPPY_AUTH_USERNAME"] = test_username
48 | os.environ["GNIPPY_AUTH_PASSWORD"] = test_password
49 |
50 |
51 | def unset_environment_config_vars():
52 | """ Unset GNIPPY env variables. """
53 | for k in ["GNIPPY_URL", "GNIPPY_RULES_URL", "GNIPPY_AUTH_USERNAME", "GNIPPY_AUTH_PASSWORD"]:
54 | if k in os.environ:
55 | del os.environ[k]
56 |
57 |
58 | def delete_test_config():
59 | """ Delete the test config if it exists. """
60 | if os.path.isfile(test_config_path):
61 | os.remove(test_config_path)
62 |
63 |
64 | def generate_test_config_file():
65 | """ Generate a test config file at test_config_path """
66 | parser = ConfigParser.SafeConfigParser()
67 | _add_credentials(parser)
68 | _add_power_track_urls(parser)
69 | _write_config_file(parser)
70 |
71 |
72 | def generate_test_config_file_with_only_auth():
73 | """ Generate a test config file at test_config_path """
74 | parser = ConfigParser.SafeConfigParser()
75 | _add_credentials(parser)
76 | _write_config_file(parser)
77 |
78 |
79 | def generate_test_config_file_with_only_powertrack():
80 | """ Generate a test config file at test_config_path """
81 | parser = ConfigParser.SafeConfigParser()
82 | _add_power_track_urls(parser)
83 | _write_config_file(parser)
84 |
85 |
86 | def get_current_username():
87 | return pwd.getpwuid(os.getuid())[0]
88 |
89 |
90 | def get_possible_home_dirs():
91 | username = get_current_username()
92 | return [i % username for i in ["/Users/%s", "/home/%s"]]
93 |
94 |
95 | def get_possible_config_locations():
96 | return [os.path.join(i, ".gnippy") for i in get_possible_home_dirs()]
97 |
98 |
99 | def os_file_exists_false(path):
100 | """ Used to patch the os.path.isfile method in tests. """
101 | return False
102 |
103 |
104 | class Response():
105 | """ Various sub-classes of Response are used by Mocks throughout tests. """
106 | status_code = None
107 | text = None
108 | json_dict = None
109 |
110 | def __init__(self, response_code, text, json):
111 | self.status_code = response_code
112 | self.text = text
113 | if json:
114 | self.json_dict = json
115 |
116 | def json(self):
117 | return self.json_dict
118 |
119 |
120 | class BadResponse(Response):
121 | def __init__(self, response_code=500, text="Internal Server Error", json=None):
122 | Response.__init__(self, response_code, text, json)
123 |
124 |
125 | class GoodResponseJsonError(Response):
126 | """ A failed request that raises an exception when the JSON method is called. """
127 | def __init__(self, response_code=200, text="All OK"):
128 | Response.__init__(self, response_code, text, None)
129 |
130 | def json(self):
131 | raise Exception("This Exception was raised intentionally")
132 |
133 |
134 | class GoodResponse(Response):
135 | def __init__(self, response_code=200, text="All OK", json=None):
136 | Response.__init__(self, response_code, text, json)
--------------------------------------------------------------------------------
/gnippy/config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | try:
4 | import configparser as ConfigParser
5 |
6 | except ImportError:
7 | import ConfigParser
8 |
9 | import os
10 |
11 | from gnippy.errors import ConfigFileNotFoundException, IncompleteConfigurationException
12 |
13 |
14 | # These are all the configurable settings by setting
15 | VALID_OPTIONS = {
16 | "Credentials": ('username', 'password'),
17 | "PowerTrack": ('url', 'rules_url')
18 | }
19 |
20 |
21 | def get_default_config_file_path():
22 | """
23 | Returns the absolute path to the default placement of the
24 | config file (~/.gnippy)
25 | """
26 | # --- This section has been borrowed from boto ------------------------
27 | # Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
28 | # Copyright (c) 2011 Chris Moyer http://coredumped.org/
29 | # https://raw.github.com/boto/boto/develop/boto/pyami/config.py
30 | #
31 | # If running in Google App Engine there is no "user" and
32 | # os.path.expanduser() will fail. Attempt to detect this case and use a
33 | # no-op expanduser function in this case.
34 | try:
35 | os.path.expanduser('~')
36 | expanduser = os.path.expanduser
37 | except (AttributeError, ImportError):
38 | # This is probably running on App Engine.
39 | expanduser = (lambda x: x)
40 | # ---End borrowed section ---------------------------------------------
41 | return os.path.join(expanduser("~"), ".gnippy")
42 |
43 |
44 | def get_config(config_file_path=None):
45 | """
46 | Parses the .gnippy file at the provided location.
47 | Returns a dictionary with all the possible configuration options,
48 | with None for the options that were not provided.
49 | """
50 | if not os.path.isfile(config_file_path):
51 | raise ConfigFileNotFoundException("Could not find %s" % config_file_path)
52 |
53 | # Attempt to parse the config file
54 | result = {}
55 | parser = ConfigParser.SafeConfigParser()
56 | parser.read(config_file_path)
57 |
58 | for section in VALID_OPTIONS:
59 | keys = VALID_OPTIONS[section]
60 | values = {}
61 | for key in keys:
62 | try:
63 | values[key] = parser.get(section, key)
64 | except ConfigParser.NoOptionError:
65 | values[key] = None
66 | except ConfigParser.NoSectionError:
67 | values[key] = None
68 |
69 | result[section] = values
70 |
71 | return result
72 |
73 |
74 | def resolve(kwarg_dict):
75 | """
76 | Look for auth and url info in the kwargs.
77 | If they don't exist check for the environment variables
78 | GNIPPY_URL
79 | GNIPPY_RULES_URL
80 | GNIPPY_AUTH_USERNAME
81 | GNIPPY_AUTH_PASSWORD
82 | If they don't exist, look for a config file path and resolve auth & url info from it.
83 | If no config file path exists, try to load the config file from the default path.
84 | If this method returns without errors, the dictionary is guaranteed to contain:
85 | {
86 | "auth": ("username", "password"),
87 | "url": "PowerTrackUrl"
88 | }
89 | """
90 | conf = {}
91 | if "auth" in kwarg_dict:
92 | conf['auth'] = kwarg_dict['auth']
93 | else:
94 | username = os.getenv("GNIPPY_AUTH_USERNAME")
95 | password = os.getenv("GNIPPY_AUTH_PASSWORD")
96 |
97 | if username and password:
98 | conf['auth'] = (username, password)
99 |
100 | if "url" in kwarg_dict:
101 | conf['url'] = kwarg_dict['url']
102 | elif os.getenv("GNIPPY_URL"):
103 | conf['url'] = os.getenv("GNIPPY_URL")
104 |
105 | if 'rules_url' in kwarg_dict:
106 | conf['rules_url'] = kwarg_dict['rules_url']
107 | elif os.getenv('GNIPPY_RULES_URL'):
108 | conf['rules_url'] = os.getenv("GNIPPY_RULES_URL")
109 |
110 | if "auth" not in conf or ("url" not in conf and 'rules_url' not in conf):
111 |
112 | if "config_file_path" in kwarg_dict:
113 | file_conf = get_config(config_file_path=kwarg_dict['config_file_path'])
114 |
115 | else:
116 | file_conf = get_config(config_file_path=get_default_config_file_path())
117 |
118 | if "auth" not in conf:
119 | creds = file_conf['Credentials']
120 | if creds['username'] and creds['password']:
121 | conf['auth'] = (creds['username'], creds['password'])
122 | else:
123 | raise IncompleteConfigurationException(
124 | "Incomplete authentication information provided. "
125 | "Please provide a username and password.")
126 |
127 | if "url" not in conf and file_conf['PowerTrack']['url']:
128 | # Not raising an exception on missing url, because user may
129 | # only want to manage rules, so streaming url is optional.
130 | conf['url'] = file_conf['PowerTrack']['url']
131 |
132 | if "rules_url" not in conf and file_conf['PowerTrack']['rules_url']:
133 | # Not raising an exception on missing rules_url, because user may
134 | # only want to consume streaming data with PowerTrack client,
135 | # so rules_url is optional.
136 | conf['rules_url'] = file_conf['PowerTrack']['rules_url']
137 |
138 | if 'url' not in conf and 'rules_url' not in conf:
139 | raise IncompleteConfigurationException("Please provide a PowerTrack url or rules_url.")
140 |
141 | return conf
142 |
--------------------------------------------------------------------------------
/gnippy/rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import json
4 |
5 | try:
6 | from urllib.parse import urlparse
7 | except:
8 | from urlparse import urlparse
9 |
10 | import requests
11 | from six import string_types
12 |
13 | from gnippy import config
14 | from gnippy.errors import *
15 |
16 |
17 | def _generate_post_object(rules_list):
18 | """ Generate the JSON object that gets posted to the Rules API. """
19 | if isinstance(rules_list, list):
20 | return { "rules": rules_list }
21 | else:
22 | raise BadArgumentException("rules_list must be of type list")
23 |
24 |
25 | def _check_rules_list(rules_list):
26 | """ Checks a rules_list to ensure that all rules are in the correct format. """
27 | def fail():
28 | msg = "rules_list is not in the correct format. Please use build_rule to build your rules list."
29 | raise RulesListFormatException(msg)
30 |
31 | if not isinstance(rules_list, list):
32 | fail()
33 |
34 | expected = ("value", "tag", "id", "id_str")
35 | for r in rules_list:
36 | if not isinstance(r, dict):
37 | fail()
38 |
39 | if "value" not in r:
40 | fail()
41 |
42 | if not isinstance(r['value'], string_types):
43 | fail()
44 |
45 | if "tag" in r:
46 | rule_tag = r['tag']
47 | if rule_tag is None or isinstance(rule_tag, string_types):
48 | pass
49 | else:
50 | fail()
51 |
52 | for k in r:
53 | if k not in expected:
54 | fail()
55 |
56 |
57 | def _post(conf, built_rules):
58 | """
59 | Generate the Rules URL and POST data and make the POST request.
60 | POST data must look like:
61 | {
62 | "rules": [
63 | {"value":"rule1", "tag":"tag1"},
64 | {"value":"rule2"}
65 | ]
66 | }
67 |
68 | Args:
69 | conf: A configuration object that contains auth and url info.
70 | built_rules: A single or list of built rules.
71 | """
72 | _check_rules_list(built_rules)
73 | rules_url = conf['rules_url']
74 | post_data = json.dumps(_generate_post_object(built_rules))
75 | r = requests.post(rules_url, auth=conf['auth'], data=post_data)
76 | if not r.status_code in range(200, 300):
77 | error_text = "HTTP Response Code: %s, Text: '%s'" % (str(r.status_code), r.text)
78 | raise RuleAddFailedException(error_text)
79 |
80 |
81 | def _generate_delete_url(conf):
82 | """
83 | Generate the Rules URL for a DELETE request.
84 | """
85 | rules_url = conf['rules_url']
86 | parsed_url = urlparse(rules_url)
87 | query = parsed_url.query
88 | if query != '':
89 | return rules_url.replace(query, query + "&_method=delete")
90 | else:
91 | return rules_url + "?_method=delete"
92 |
93 |
94 | def _delete(conf, built_rules):
95 | """
96 | Generate the Delete Rules URL and make a POST request.
97 | POST data must look like:
98 | {
99 | "rules": [
100 | {"value":"rule1", "tag":"tag1"},
101 | {"value":"rule2"}
102 | ]
103 | }
104 |
105 | Args:
106 | conf: A configuration object that contains auth and url info.
107 | built_rules: A single or list of built rules.
108 | """
109 | _check_rules_list(built_rules)
110 | rules_url = _generate_delete_url(conf)
111 | delete_data = json.dumps(_generate_post_object(built_rules))
112 | r = requests.post(rules_url, auth=conf['auth'], data=delete_data)
113 | if not r.status_code in range(200,300):
114 | error_text = "HTTP Response Code: %s, Text: '%s'" % (str(r.status_code), r.text)
115 | raise RuleDeleteFailedException(error_text)
116 |
117 |
118 | def build(rule_string, tag=None):
119 | """
120 | Takes a rule string and optional tag and turns it into a "built_rule" that looks like:
121 | { "value": "rule string", "tag": "my tag" }
122 | """
123 | if rule_string is None:
124 | raise BadArgumentException("rule_string cannot be None")
125 | rule = { "value": rule_string }
126 | if tag:
127 | rule['tag'] = tag
128 | return rule
129 |
130 |
131 | def add_rule(rule_string, tag=None, **kwargs):
132 | """ Synchronously add a single rule to GNIP PowerTrack. """
133 | conf = config.resolve(kwargs)
134 | rule = build(rule_string, tag)
135 | rules_list = [rule,]
136 | _post(conf, rules_list)
137 |
138 |
139 | def add_rules(rules_list, **kwargs):
140 | """ Synchronously add multiple rules to GNIP PowerTrack in one go. """
141 | conf = config.resolve(kwargs)
142 | _post(conf, rules_list)
143 |
144 |
145 | def get_rules(**kwargs):
146 | """
147 | Get all the rules currently applied to PowerTrack.
148 | Optional Args:
149 | rules_url: Specify this arg if you're working with a PowerTrack connection that's not listed in your .gnippy file.
150 | auth: Specify this arg if you want to override the credentials in your .gnippy file.
151 |
152 | Returns:
153 | A list of currently applied rules in the form:
154 | [
155 | { "value": "(Hello OR World) AND lang:en" },
156 | { "value": "Hello", "tag": "mytag" }
157 | ]
158 | """
159 | conf = config.resolve(kwargs)
160 | rules_url = conf['rules_url']
161 |
162 | def fail(reason):
163 | raise RulesGetFailedException("Could not get current rules for '%s'. Reason: '%s'" % (rules_url, reason))
164 |
165 | try:
166 | r = requests.get(rules_url, auth=conf['auth'])
167 | except Exception as e:
168 | fail(str(e))
169 |
170 | if r.status_code not in range(200,300):
171 | fail("HTTP Status Code: %s" % r.status_code)
172 |
173 | try:
174 | rules_json = r.json()
175 | except:
176 | fail("GNIP API returned malformed JSON")
177 |
178 | if "rules" in rules_json:
179 | return rules_json['rules']
180 | else:
181 | fail("GNIP API response did not return a rules object")
182 |
183 |
184 | def delete_rule(rule_dict, **kwargs):
185 | """ Synchronously delete a single rule from GNIP PowerTrack. """
186 | conf = config.resolve(kwargs)
187 | rules_list = [rule_dict,]
188 | _delete(conf, rules_list)
189 |
190 |
191 | def delete_rules(rules_list, **kwargs):
192 | """ Synchronously delete multiple rules from GNIP PowerTrack. """
193 | conf = config.resolve(kwargs)
194 | _delete(conf, rules_list)
195 |
--------------------------------------------------------------------------------
/CLA/j-bennet.txt:
--------------------------------------------------------------------------------
1 | Individual Contributor License Agreement ("Agreement") V2.0
2 | http://www.apache.org/licenses/
3 |
4 | Thank you for your interest in gnippy (the
5 | "Project"). In order to clarify the intellectual property license
6 | granted with Contributions from any person or entity, the Project
7 | must have a Contributor License Agreement ("CLA") on file that has
8 | been signed by each Contributor, indicating agreement to the license
9 | terms below. This license is for your protection as a Contributor as
10 | well as the protection of the Project and its users; it does not
11 | change your rights to use your own Contributions for any other purpose.
12 | Please read this document carefully before signing and keep a copy for
13 | your records.
14 |
15 | Full name: Irina Truong
16 |
17 | GitHub username: j-bennet
18 |
19 |
20 | You accept and agree to the following terms and conditions for Your
21 | present and future Contributions submitted to the Project. In
22 | return, the Project shall not use Your Contributions in a way that
23 | is contrary to the public benefit or inconsistent with its nonprofit
24 | status and bylaws in effect at the time of the Contribution. Except
25 | for the license granted herein to the Project and recipients of
26 | software distributed by the Project, You reserve all right, title,
27 | and interest in and to Your Contributions.
28 |
29 | 1. Definitions.
30 |
31 | "You" (or "Your") shall mean the copyright owner or legal entity
32 | authorized by the copyright owner that is making this Agreement
33 | with the Project. For legal entities, the entity making a
34 | Contribution and all other entities that control, are controlled
35 | by, or are under common control with that entity are considered to
36 | be a single Contributor. For the purposes of this definition,
37 | "control" means (i) the power, direct or indirect, to cause the
38 | direction or management of such entity, whether by contract or
39 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
40 | outstanding shares, or (iii) beneficial ownership of such entity.
41 |
42 | "Contribution" shall mean any original work of authorship,
43 | including any modifications or additions to an existing work, that
44 | is intentionally submitted by You to the Project for inclusion
45 | in, or documentation of, any of the products owned or managed by
46 | the Project (the "Work"). For the purposes of this definition,
47 | "submitted" means any form of electronic, verbal, or written
48 | communication sent to the Project or its representatives,
49 | including but not limited to communication on electronic mailing
50 | lists, source code control systems, and issue tracking systems that
51 | are managed by, or on behalf of, the Project for the purpose of
52 | discussing and improving the Work, but excluding communication that
53 | is conspicuously marked or otherwise designated in writing by You
54 | as "Not a Contribution."
55 |
56 | 2. Grant of Copyright License. Subject to the terms and conditions of
57 | this Agreement, You hereby grant to the Project and to
58 | recipients of software distributed by the Project a perpetual,
59 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
60 | copyright license to reproduce, prepare derivative works of,
61 | publicly display, publicly perform, sublicense, and distribute Your
62 | Contributions and such derivative works.
63 |
64 | 3. Grant of Patent License. Subject to the terms and conditions of
65 | this Agreement, You hereby grant to the Project and to
66 | recipients of software distributed by the Project a perpetual,
67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
68 | (except as stated in this section) patent license to make, have
69 | made, use, offer to sell, sell, import, and otherwise transfer the
70 | Work, where such license applies only to those patent claims
71 | licensable by You that are necessarily infringed by Your
72 | Contribution(s) alone or by combination of Your Contribution(s)
73 | with the Work to which such Contribution(s) was submitted. If any
74 | entity institutes patent litigation against You or any other entity
75 | (including a cross-claim or counterclaim in a lawsuit) alleging
76 | that your Contribution, or the Work to which you have contributed,
77 | constitutes direct or contributory patent infringement, then any
78 | patent licenses granted to that entity under this Agreement for
79 | that Contribution or Work shall terminate as of the date such
80 | litigation is filed.
81 |
82 | 4. You represent that you are legally entitled to grant the above
83 | license. If your employer(s) has rights to intellectual property
84 | that you create that includes your Contributions, you represent
85 | that you have received permission to make Contributions on behalf
86 | of that employer, that your employer has waived such rights for
87 | your Contributions to the Project, or that your employer has
88 | executed a separate Corporate CLA with the Project.
89 |
90 | 5. You represent that each of Your Contributions is Your original
91 | creation (see section 7 for submissions on behalf of others). You
92 | represent that Your Contribution submissions include complete
93 | details of any third-party license or other restriction (including,
94 | but not limited to, related patents and trademarks) of which you
95 | are personally aware and which are associated with any part of Your
96 | Contributions.
97 |
98 | 6. You are not expected to provide support for Your Contributions,
99 | except to the extent You desire to provide support. You may provide
100 | support for free, for a fee, or not at all. Unless required by
101 | applicable law or agreed to in writing, You provide Your
102 | Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
103 | OF ANY KIND, either express or implied, including, without
104 | limitation, any warranties or conditions of TITLE, NON-
105 | INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
106 |
107 | 7. Should You wish to submit work that is not Your original creation,
108 | You may submit it to the Project separately from any
109 | Contribution, identifying the complete details of its source and of
110 | any license or other restriction (including, but not limited to,
111 | related patents, trademarks, and license agreements) of which you
112 | are personally aware, and conspicuously marking the work as
113 | "Submitted on behalf of a third-party: [named here]".
114 |
115 | 8. You agree to notify the Project of any facts or circumstances of
116 | which you become aware that would make these representations
117 | inaccurate in any respect.
118 |
119 | Please sign: Irina Truong Date: 01/06/2016
120 |
--------------------------------------------------------------------------------
/CLA/mu.txt:
--------------------------------------------------------------------------------
1 | Individual Contributor License Agreement ("Agreement") V2.0
2 | http://www.apache.org/licenses/
3 |
4 | Thank you for your interest in gnippy (the
5 | "Project"). In order to clarify the intellectual property license
6 | granted with Contributions from any person or entity, the Project
7 | must have a Contributor License Agreement ("CLA") on file that has
8 | been signed by each Contributor, indicating agreement to the license
9 | terms below. This license is for your protection as a Contributor as
10 | well as the protection of the Project and its users; it does not
11 | change your rights to use your own Contributions for any other purpose.
12 | Please read this document carefully before signing and keep a copy for
13 | your records.
14 |
15 | Full name: Mark Unsworth
16 |
17 | GitHub username: markunsworth
18 |
19 |
20 | You accept and agree to the following terms and conditions for Your
21 | present and future Contributions submitted to the Project. In
22 | return, the Project shall not use Your Contributions in a way that
23 | is contrary to the public benefit or inconsistent with its nonprofit
24 | status and bylaws in effect at the time of the Contribution. Except
25 | for the license granted herein to the Project and recipients of
26 | software distributed by the Project, You reserve all right, title,
27 | and interest in and to Your Contributions.
28 |
29 | 1. Definitions.
30 |
31 | "You" (or "Your") shall mean the copyright owner or legal entity
32 | authorized by the copyright owner that is making this Agreement
33 | with the Project. For legal entities, the entity making a
34 | Contribution and all other entities that control, are controlled
35 | by, or are under common control with that entity are considered to
36 | be a single Contributor. For the purposes of this definition,
37 | "control" means (i) the power, direct or indirect, to cause the
38 | direction or management of such entity, whether by contract or
39 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
40 | outstanding shares, or (iii) beneficial ownership of such entity.
41 |
42 | "Contribution" shall mean any original work of authorship,
43 | including any modifications or additions to an existing work, that
44 | is intentionally submitted by You to the Project for inclusion
45 | in, or documentation of, any of the products owned or managed by
46 | the Project (the "Work"). For the purposes of this definition,
47 | "submitted" means any form of electronic, verbal, or written
48 | communication sent to the Project or its representatives,
49 | including but not limited to communication on electronic mailing
50 | lists, source code control systems, and issue tracking systems that
51 | are managed by, or on behalf of, the Project for the purpose of
52 | discussing and improving the Work, but excluding communication that
53 | is conspicuously marked or otherwise designated in writing by You
54 | as "Not a Contribution."
55 |
56 | 2. Grant of Copyright License. Subject to the terms and conditions of
57 | this Agreement, You hereby grant to the Project and to
58 | recipients of software distributed by the Project a perpetual,
59 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
60 | copyright license to reproduce, prepare derivative works of,
61 | publicly display, publicly perform, sublicense, and distribute Your
62 | Contributions and such derivative works.
63 |
64 | 3. Grant of Patent License. Subject to the terms and conditions of
65 | this Agreement, You hereby grant to the Project and to
66 | recipients of software distributed by the Project a perpetual,
67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
68 | (except as stated in this section) patent license to make, have
69 | made, use, offer to sell, sell, import, and otherwise transfer the
70 | Work, where such license applies only to those patent claims
71 | licensable by You that are necessarily infringed by Your
72 | Contribution(s) alone or by combination of Your Contribution(s)
73 | with the Work to which such Contribution(s) was submitted. If any
74 | entity institutes patent litigation against You or any other entity
75 | (including a cross-claim or counterclaim in a lawsuit) alleging
76 | that your Contribution, or the Work to which you have contributed,
77 | constitutes direct or contributory patent infringement, then any
78 | patent licenses granted to that entity under this Agreement for
79 | that Contribution or Work shall terminate as of the date such
80 | litigation is filed.
81 |
82 | 4. You represent that you are legally entitled to grant the above
83 | license. If your employer(s) has rights to intellectual property
84 | that you create that includes your Contributions, you represent
85 | that you have received permission to make Contributions on behalf
86 | of that employer, that your employer has waived such rights for
87 | your Contributions to the Project, or that your employer has
88 | executed a separate Corporate CLA with the Project.
89 |
90 | 5. You represent that each of Your Contributions is Your original
91 | creation (see section 7 for submissions on behalf of others). You
92 | represent that Your Contribution submissions include complete
93 | details of any third-party license or other restriction (including,
94 | but not limited to, related patents and trademarks) of which you
95 | are personally aware and which are associated with any part of Your
96 | Contributions.
97 |
98 | 6. You are not expected to provide support for Your Contributions,
99 | except to the extent You desire to provide support. You may provide
100 | support for free, for a fee, or not at all. Unless required by
101 | applicable law or agreed to in writing, You provide Your
102 | Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
103 | OF ANY KIND, either express or implied, including, without
104 | limitation, any warranties or conditions of TITLE, NON-
105 | INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
106 |
107 | 7. Should You wish to submit work that is not Your original creation,
108 | You may submit it to the Project separately from any
109 | Contribution, identifying the complete details of its source and of
110 | any license or other restriction (including, but not limited to,
111 | related patents, trademarks, and license agreements) of which you
112 | are personally aware, and conspicuously marking the work as
113 | "Submitted on behalf of a third-party: [named here]".
114 |
115 | 8. You agree to notify the Project of any facts or circumstances of
116 | which you become aware that would make these representations
117 | inaccurate in any respect.
118 |
119 | Please sign: Mark Unsworth Date: 17th Nov 2015
120 |
--------------------------------------------------------------------------------
/CLA/cla_template.txt:
--------------------------------------------------------------------------------
1 | Individual Contributor License Agreement ("Agreement") V2.0
2 | http://www.apache.org/licenses/
3 |
4 | Thank you for your interest in gnippy (the
5 | "Project"). In order to clarify the intellectual property license
6 | granted with Contributions from any person or entity, the Project
7 | must have a Contributor License Agreement ("CLA") on file that has
8 | been signed by each Contributor, indicating agreement to the license
9 | terms below. This license is for your protection as a Contributor as
10 | well as the protection of the Project and its users; it does not
11 | change your rights to use your own Contributions for any other purpose.
12 | Please read this document carefully before signing and keep a copy for
13 | your records.
14 |
15 | Full name: __________________________________________________
16 |
17 | GitHub username: ____________________________________________
18 |
19 |
20 | You accept and agree to the following terms and conditions for Your
21 | present and future Contributions submitted to the Project. In
22 | return, the Project shall not use Your Contributions in a way that
23 | is contrary to the public benefit or inconsistent with its nonprofit
24 | status and bylaws in effect at the time of the Contribution. Except
25 | for the license granted herein to the Project and recipients of
26 | software distributed by the Project, You reserve all right, title,
27 | and interest in and to Your Contributions.
28 |
29 | 1. Definitions.
30 |
31 | "You" (or "Your") shall mean the copyright owner or legal entity
32 | authorized by the copyright owner that is making this Agreement
33 | with the Project. For legal entities, the entity making a
34 | Contribution and all other entities that control, are controlled
35 | by, or are under common control with that entity are considered to
36 | be a single Contributor. For the purposes of this definition,
37 | "control" means (i) the power, direct or indirect, to cause the
38 | direction or management of such entity, whether by contract or
39 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
40 | outstanding shares, or (iii) beneficial ownership of such entity.
41 |
42 | "Contribution" shall mean any original work of authorship,
43 | including any modifications or additions to an existing work, that
44 | is intentionally submitted by You to the Project for inclusion
45 | in, or documentation of, any of the products owned or managed by
46 | the Project (the "Work"). For the purposes of this definition,
47 | "submitted" means any form of electronic, verbal, or written
48 | communication sent to the Project or its representatives,
49 | including but not limited to communication on electronic mailing
50 | lists, source code control systems, and issue tracking systems that
51 | are managed by, or on behalf of, the Project for the purpose of
52 | discussing and improving the Work, but excluding communication that
53 | is conspicuously marked or otherwise designated in writing by You
54 | as "Not a Contribution."
55 |
56 | 2. Grant of Copyright License. Subject to the terms and conditions of
57 | this Agreement, You hereby grant to the Project and to
58 | recipients of software distributed by the Project a perpetual,
59 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
60 | copyright license to reproduce, prepare derivative works of,
61 | publicly display, publicly perform, sublicense, and distribute Your
62 | Contributions and such derivative works.
63 |
64 | 3. Grant of Patent License. Subject to the terms and conditions of
65 | this Agreement, You hereby grant to the Project and to
66 | recipients of software distributed by the Project a perpetual,
67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
68 | (except as stated in this section) patent license to make, have
69 | made, use, offer to sell, sell, import, and otherwise transfer the
70 | Work, where such license applies only to those patent claims
71 | licensable by You that are necessarily infringed by Your
72 | Contribution(s) alone or by combination of Your Contribution(s)
73 | with the Work to which such Contribution(s) was submitted. If any
74 | entity institutes patent litigation against You or any other entity
75 | (including a cross-claim or counterclaim in a lawsuit) alleging
76 | that your Contribution, or the Work to which you have contributed,
77 | constitutes direct or contributory patent infringement, then any
78 | patent licenses granted to that entity under this Agreement for
79 | that Contribution or Work shall terminate as of the date such
80 | litigation is filed.
81 |
82 | 4. You represent that you are legally entitled to grant the above
83 | license. If your employer(s) has rights to intellectual property
84 | that you create that includes your Contributions, you represent
85 | that you have received permission to make Contributions on behalf
86 | of that employer, that your employer has waived such rights for
87 | your Contributions to the Project, or that your employer has
88 | executed a separate Corporate CLA with the Project.
89 |
90 | 5. You represent that each of Your Contributions is Your original
91 | creation (see section 7 for submissions on behalf of others). You
92 | represent that Your Contribution submissions include complete
93 | details of any third-party license or other restriction (including,
94 | but not limited to, related patents and trademarks) of which you
95 | are personally aware and which are associated with any part of Your
96 | Contributions.
97 |
98 | 6. You are not expected to provide support for Your Contributions,
99 | except to the extent You desire to provide support. You may provide
100 | support for free, for a fee, or not at all. Unless required by
101 | applicable law or agreed to in writing, You provide Your
102 | Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
103 | OF ANY KIND, either express or implied, including, without
104 | limitation, any warranties or conditions of TITLE, NON-
105 | INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
106 |
107 | 7. Should You wish to submit work that is not Your original creation,
108 | You may submit it to the Project separately from any
109 | Contribution, identifying the complete details of its source and of
110 | any license or other restriction (including, but not limited to,
111 | related patents, trademarks, and license agreements) of which you
112 | are personally aware, and conspicuously marking the work as
113 | "Submitted on behalf of a third-party: [named here]".
114 |
115 | 8. You agree to notify the Project of any facts or circumstances of
116 | which you become aware that would make these representations
117 | inaccurate in any respect.
118 |
119 | Please sign: __________________________________ Date: ________________
120 |
121 |
--------------------------------------------------------------------------------
/gnippy/test/test_powertrackclient.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | from time import sleep
5 |
6 | import mock
7 |
8 | from gnippy.powertrackclient import append_backfill_to_url
9 |
10 | try:
11 | import unittest2 as unittest
12 | except ImportError:
13 | import unittest
14 |
15 | from gnippy import PowerTrackClient
16 | from gnippy.test import test_utils
17 |
18 |
19 | def _dummy_callback(activity):
20 | pass
21 |
22 |
23 | class TestException(Exception):
24 | def __init__(self, message):
25 | self.message = message
26 |
27 |
28 | def get_exception(*args, **kwargs):
29 | raise TestException("This is a test exception")
30 |
31 |
32 | def iter_lines_generator():
33 |
34 | n = 0
35 | while True:
36 |
37 | yield "arbitrary string value {0}".format(n)
38 | n += 1
39 |
40 | def get_request_stream(url, auth, stream):
41 |
42 | mocked_stream = mock.MagicMock()
43 |
44 | p = mock.PropertyMock()
45 | p.return_value = 200
46 |
47 | type(mocked_stream).status_code = p
48 |
49 | mocked_stream.iter_lines.side_effect = iter_lines_generator
50 |
51 | return mocked_stream
52 |
53 |
54 | config_file = test_utils.test_config_path
55 |
56 |
57 | class PowerTrackClientTestCase(unittest.TestCase):
58 |
59 | def setUp(self):
60 | pass
61 |
62 | def tearDown(self):
63 | """ Remove the test config file. """
64 | test_utils.delete_test_config()
65 |
66 | def test_constructor_only_file(self):
67 | """ Initialize PowerTrackClient with only a config file path. """
68 | test_utils.generate_test_config_file()
69 | client = PowerTrackClient(_dummy_callback, config_file_path=config_file)
70 | expected_auth = (test_utils.test_username, test_utils.test_password)
71 | expected_url = test_utils.test_powertrack_url
72 |
73 | self.assertEqual(expected_auth[0], client.auth[0])
74 | self.assertEqual(expected_auth[1], client.auth[1])
75 | self.assertEqual(expected_url, client.url)
76 |
77 | def test_constructor_only_url(self):
78 | """
79 | Initialize PowerTrackClient with only urls.
80 | The config file is provided for testability.
81 | """
82 | test_utils.generate_test_config_file_with_only_auth()
83 |
84 | client = PowerTrackClient(_dummy_callback,
85 | url=test_utils.test_powertrack_url,
86 | config_file_path=config_file)
87 | expected_auth = (test_utils.test_username, test_utils.test_password)
88 | expected_url = test_utils.test_powertrack_url
89 |
90 | self.assertEqual(expected_auth[0], client.auth[0])
91 | self.assertEqual(expected_auth[1], client.auth[1])
92 | self.assertEqual(expected_url, client.url)
93 |
94 | def test_constructor_only_auth(self):
95 | """
96 | Initialize PowerTrackClient with only a config the auth tuple.
97 | The config file is provided for testability.
98 | """
99 | test_utils.generate_test_config_file_with_only_powertrack()
100 |
101 | expected_auth = (test_utils.test_username, test_utils.test_password)
102 | expected_url = test_utils.test_powertrack_url
103 | client = PowerTrackClient(_dummy_callback, auth=expected_auth, config_file_path=config_file)
104 |
105 | self.assertEqual(expected_auth[0], client.auth[0])
106 | self.assertEqual(expected_auth[1], client.auth[1])
107 | self.assertEqual(expected_url, client.url)
108 |
109 | def test_constructor_all_args(self):
110 | """ Initialize PowerTrackClient with all args. """
111 | test_utils.generate_test_config_file()
112 | expected_auth = ("hello", "world")
113 | expected_url = "http://wat.com/testing.json"
114 | client = PowerTrackClient(_dummy_callback, auth=expected_auth, url=expected_url, config_file_path=config_file)
115 |
116 | self.assertEqual(expected_auth[0], client.auth[0])
117 | self.assertEqual(expected_auth[1], client.auth[1])
118 | self.assertEqual(expected_url, client.url)
119 |
120 | def test_no_args(self):
121 | """ Check if a ~/.gnippy file is present and run a no-arg test. """
122 | possible_paths = test_utils.get_possible_config_locations()
123 | for config_path in possible_paths:
124 | if os.path.isfile(config_path):
125 | client = PowerTrackClient(_dummy_callback)
126 | self.assertIsNotNone(client.auth)
127 | self.assertIsNotNone(client.url)
128 | self.assertTrue("http" in client.url and "://" in client.url)
129 |
130 | @mock.patch('requests.get', get_exception)
131 | def test_exception_with_callback(self):
132 | """ When exception_callback is provided, worker uses it to communicate errors. """
133 | test_utils.generate_test_config_file()
134 |
135 | exception_callback = mock.Mock()
136 |
137 | client = PowerTrackClient(_dummy_callback, exception_callback,
138 | config_file_path=config_file)
139 | client.connect()
140 | client.disconnect()
141 |
142 | self.assertTrue(exception_callback.called)
143 | actual_exinfo = exception_callback.call_args[0][0]
144 | actual_ex = actual_exinfo[1]
145 | self.assertIsInstance(actual_ex, Exception)
146 | self.assertEqual(actual_ex.message, "This is a test exception")
147 |
148 | @mock.patch('requests.get', get_exception)
149 | def test_connected_status_after_exception_is_false(self):
150 | """ When an exception is thrown the client is no longer connected. """
151 | test_utils.generate_test_config_file()
152 |
153 | client = PowerTrackClient(_dummy_callback,
154 | config_file_path=config_file)
155 | client.connect()
156 |
157 | sleep(2)
158 |
159 | self.assertFalse(client.connected())
160 |
161 | @mock.patch('requests.get', get_request_stream)
162 | def test_connected_status_true_when_running(self):
163 | """ Once the client connect method is called the client reports as connected. """
164 | test_utils.generate_test_config_file()
165 |
166 | client = PowerTrackClient(_dummy_callback,
167 | config_file_path=config_file)
168 |
169 | client.connect()
170 |
171 | self.assertTrue(client.connected())
172 |
173 | client.disconnect()
174 |
175 | self.assertFalse(client.connected())
176 |
177 | @mock.patch('requests.get')
178 | def test_backfill_value_appended_to_url(self, mocked_requests_get):
179 | """When passing a backfill value it is appended to the url call to
180 | GNIP"""
181 |
182 | backfill_minutes = 3
183 |
184 | expected_powertrack_url = "{0}?backfillMinutes={1}".format(
185 | test_utils.test_powertrack_url, backfill_minutes)
186 |
187 | test_utils.generate_test_config_file()
188 |
189 | client = PowerTrackClient(_dummy_callback, config_file_path=config_file)
190 |
191 | client.connect(backfill_minutes=backfill_minutes)
192 | client.disconnect()
193 |
194 | mocked_requests_get.assert_called_with(
195 | expected_powertrack_url,
196 | auth=(test_utils.test_username, test_utils.test_password),
197 | stream=True
198 | )
199 |
200 | def test_non_integer_backfill_value_raises_exception(self):
201 | """When passing a backfill value that isn't an integer raises an
202 | exception"""
203 |
204 | backfill_minutes = "FOUR"
205 |
206 | test_utils.generate_test_config_file()
207 |
208 | client = PowerTrackClient(_dummy_callback, config_file_path=config_file)
209 |
210 | self.assertRaises(AssertionError, client.connect, backfill_minutes)
211 |
212 | def test_backfill_value_greater_than_five_raises_exception(self):
213 | """When passing a backfill value that isn't an integer raises an
214 | exception"""
215 |
216 | backfill_minutes = 12
217 |
218 | test_utils.generate_test_config_file()
219 |
220 | client = PowerTrackClient(_dummy_callback, config_file_path=config_file)
221 |
222 | self.assertRaises(AssertionError, client.connect, backfill_minutes)
223 |
224 | def test_append_backfill_to_url_appends_backfillMinutes_param(self):
225 |
226 | base_url = "http://www.twitter.com"
227 | backfill_minutes = 2
228 | expected_url = "http://www.twitter.com?backfillMinutes=2"
229 |
230 | returned_value = append_backfill_to_url(base_url, backfill_minutes)
231 |
232 | self.assertEqual(returned_value, expected_url)
233 |
234 | def test_append_backfill_to_url_replaces_existing_backfillMinutes_param(
235 | self):
236 |
237 | base_url = "http://www.twitter.com?backfillMinutes=5"
238 | backfill_minutes = 1
239 | expected_url = "http://www.twitter.com?backfillMinutes=1"
240 |
241 | returned_value = append_backfill_to_url(base_url, backfill_minutes)
242 |
243 | self.assertEqual(returned_value, expected_url)
--------------------------------------------------------------------------------
/gnippy/test/test_rules.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | try:
4 | import unittest2 as unittest
5 | except ImportError:
6 | import unittest
7 |
8 | import mock
9 |
10 | from gnippy import rules
11 | from gnippy.errors import *
12 | from gnippy.test import test_utils
13 |
14 |
15 | # Mocks
16 | def bad_post(url, auth, data):
17 | return test_utils.BadResponse()
18 |
19 |
20 | def good_post(url, auth, data):
21 | return test_utils.GoodResponse()
22 |
23 |
24 | def bad_get(url, auth):
25 | return test_utils.BadResponse()
26 |
27 |
28 | def get_exception(url, auth):
29 | raise Exception("This is a test exception")
30 |
31 |
32 | def get_json_exception(url, auth):
33 | return test_utils.GoodResponseJsonError()
34 |
35 |
36 | def good_get_no_rules_field(url, auth):
37 | return test_utils.GoodResponse(json={"hello": "world"})
38 |
39 |
40 | def good_get_no_rules(url, auth):
41 | return test_utils.GoodResponse(json={"rules": []})
42 |
43 |
44 | def good_get_one_rule(url, auth):
45 | return test_utils.GoodResponse(
46 | json={"rules": [{"value": "Hello", "tag": "mytag"}]})
47 |
48 |
49 | def bad_delete(url, auth, data):
50 | return test_utils.BadResponse()
51 |
52 |
53 | def good_delete(url, auth, data):
54 | return test_utils.GoodResponse()
55 |
56 |
57 | class RulesTestCase(unittest.TestCase):
58 | rule_string = "Hello OR World"
59 | tag = "my_tag"
60 |
61 | def _generate_rules_list(self):
62 | rules_list = []
63 | rules_list.append(rules.build(self.rule_string))
64 | rules_list.append(rules.build(self.rule_string, self.tag))
65 | return rules_list
66 |
67 | def setUp(self):
68 | test_utils.generate_test_config_file()
69 |
70 | def tearDown(self):
71 | test_utils.delete_test_config()
72 |
73 | def test_build_post_object(self):
74 | """ Generate rules object to post. """
75 | rules_list = self._generate_rules_list()
76 | post_obj = rules._generate_post_object(rules_list)
77 | self.assertTrue("rules" in post_obj)
78 | rules._check_rules_list(post_obj['rules'])
79 |
80 | def test_check_one_rule_ok(self):
81 | """ Check list of one rule. """
82 | l = [{"value": "hello"}]
83 | rules._check_rules_list(l)
84 |
85 | def test_check_many_rules_ok(self):
86 | """ Check list of many rules. """
87 | l = [
88 | {"value": "hello", "id": 3},
89 | {"value": "goodbye", "tag": "w", "id": 4},
90 | {"value": "hi again", "tag": "x"},
91 | {"value": "bye again"}
92 | ]
93 | rules._check_rules_list(l)
94 |
95 | def test_check_rule_tag_none(self):
96 | """ Check list of rules both with tag and without. """
97 | l = [{"value": "hello", "tag": None}, {"value": "h", "tag": "w"}]
98 | rules._check_rules_list(l)
99 |
100 | def test_check_one_rule_typo_values(self):
101 | """ Make sure incorectly formatted rule values fail. """
102 | l = [{"values": "hello"}]
103 | try:
104 | rules._check_rules_list(l)
105 | except RulesListFormatException:
106 | return
107 | self.fail(
108 | "_check_rules_list was supposed to throw a RuleFormatException")
109 |
110 | def test_check_one_rule_typo_tag(self):
111 | """ Make sure incorrectly formatted rule tags fail. """
112 | l = [{"value": "hello", "tags": "t"}]
113 | try:
114 | rules._check_rules_list(l)
115 | except RulesListFormatException:
116 | return
117 | self.fail(
118 | "_check_rules_list was supposed to throw a RuleFormatException")
119 |
120 | def test_check_one_rule_extra_stuff_in_rule(self):
121 | """ Make sure rules with unexpected keys fail. """
122 | l = [{"value": "hello", "wat": "man"}]
123 | try:
124 | rules._check_rules_list(l)
125 | except RulesListFormatException:
126 | return
127 | self.fail(
128 | "_check_rules_list was supposed to throw a RuleFormatException")
129 |
130 | def test_build_rule_bad_args(self):
131 | """ Make sure building rules with unexpected args fail. """
132 | try:
133 | rules.build(None)
134 | except BadArgumentException:
135 | return
136 | self.fail(
137 | "rules.build_rule was supposed to throw a BadArgumentException")
138 |
139 | def test_build_rule_without_tag(self):
140 | """ Build rule without tag. """
141 | r = rules.build(self.rule_string)
142 | self.assertEqual(r['value'], self.rule_string)
143 | self.assertFalse("tag" in r)
144 | rules._check_rules_list([r])
145 |
146 | def test_build_rule_with_tag(self):
147 | """ Build rule with tag. """
148 | r = rules.build(self.rule_string, tag=self.tag)
149 | self.assertEqual(r['value'], self.rule_string)
150 | self.assertEqual(r['tag'], self.tag)
151 | rules._check_rules_list([r])
152 |
153 | @mock.patch('os.path.isfile', test_utils.os_file_exists_false)
154 | @mock.patch('requests.post', good_post)
155 | def test_add_one_rule_no_creds(self):
156 | """ Make sure adding rule without credentials fail. """
157 | try:
158 | rules.add_rule(self.rule_string, self.tag)
159 | except ConfigFileNotFoundException:
160 | return
161 | self.fail(
162 | "Rule Add was supposed to fail and throw a ConfigFileNotFoundException")
163 |
164 | @mock.patch('requests.post', good_post)
165 | def test_add_one_rule_ok(self):
166 | """Add one rule with config. """
167 | rules.add_rule(self.rule_string, self.tag,
168 | config_file_path=test_utils.test_config_path)
169 |
170 | @mock.patch('requests.post', bad_post)
171 | def test_add_one_rule_not_ok(self):
172 | """Add one rule with exception thrown. """
173 | try:
174 | rules.add_rule(self.rule_string, self.tag,
175 | config_file_path=test_utils.test_config_path)
176 | except RuleAddFailedException:
177 | return
178 | self.fail("Rule Add was supposed to fail and throw a RuleAddException")
179 |
180 | @mock.patch('os.path.isfile', test_utils.os_file_exists_false)
181 | @mock.patch('requests.post', good_post)
182 | def test_add_many_rules_no_creds(self):
183 | """ Make sure adding rules with non-existent config fails. """
184 | try:
185 | rules.add_rule(self.rule_string, self.tag)
186 | except ConfigFileNotFoundException:
187 | return
188 | self.fail(
189 | "Rule Add was supposed to fail and throw a ConfigFileNotFoundException")
190 |
191 | @mock.patch('requests.post', good_post)
192 | def test_add_many_rules_ok(self):
193 | """ Add many rules. """
194 | rules_list = self._generate_rules_list()
195 | rules.add_rules(rules_list,
196 | config_file_path=test_utils.test_config_path)
197 |
198 | @mock.patch('requests.post', bad_post)
199 | def test_add_many_rules_not_ok(self):
200 | """ Add many rules with exception thrown. """
201 | try:
202 | rules_list = self._generate_rules_list()
203 | rules.add_rules(rules_list,
204 | config_file_path=test_utils.test_config_path)
205 | except RuleAddFailedException:
206 | return
207 | self.fail("Rule Add was supposed to fail and throw a RuleAddException")
208 |
209 | @mock.patch('requests.get', get_exception)
210 | def test_get_rules_requests_get_exception(self):
211 | """ Get rules with exception thrown. """
212 | try:
213 | r = rules.get_rules(config_file_path=test_utils.test_config_path)
214 | except RulesGetFailedException:
215 | return
216 | self.fail("rules.get() was supposed to throw a RulesGetFailedException")
217 |
218 | @mock.patch('requests.get', bad_get)
219 | def test_get_rules_bad_status_code(self):
220 | """ Get rules with error response. """
221 | try:
222 | r = rules.get_rules(config_file_path=test_utils.test_config_path)
223 | except RulesGetFailedException as e:
224 | self.assertTrue("HTTP Status Code" in str(e))
225 | return
226 | self.fail("rules.get() was supposed to throw a RulesGetFailedException")
227 |
228 | @mock.patch('requests.get', get_json_exception)
229 | def test_get_rules_bad_json(self):
230 | """ Get rules with bad json response. """
231 | try:
232 | r = rules.get_rules(config_file_path=test_utils.test_config_path)
233 | except RulesGetFailedException as e:
234 | self.assertTrue("GNIP API returned malformed JSON" in str(e))
235 | return
236 | self.fail("rules.get() was supposed to throw a RulesGetFailedException")
237 |
238 | @mock.patch('requests.get', good_get_no_rules_field)
239 | def test_get_rules_no_rules_field_json(self):
240 | """ Get rules with invalid response. """
241 | try:
242 | r = rules.get_rules(config_file_path=test_utils.test_config_path)
243 | except RulesGetFailedException as e:
244 | self.assertTrue(
245 | "GNIP API response did not return a rules object" in str(e))
246 | return
247 | self.fail("rules.get() was supposed to throw a RulesGetFailedException")
248 |
249 | @mock.patch('requests.get', good_get_no_rules)
250 | def test_get_rules_success_no_rules(self):
251 | """ Get rules with empty response. """
252 | r = rules.get_rules(config_file_path=test_utils.test_config_path)
253 | self.assertEqual(0, len(r))
254 |
255 | @mock.patch('requests.get', good_get_one_rule)
256 | def test_get_rules_success_one_rule(self):
257 | """ Get one rule. """
258 | r = rules.get_rules(config_file_path=test_utils.test_config_path)
259 | self.assertEqual(1, len(r))
260 |
261 | @mock.patch('requests.post', good_delete)
262 | def test_delete_rules_single(self):
263 | """ Delete one rule. """
264 | rules.delete_rule({"value": "Hello World"},
265 | config_file_path=test_utils.test_config_path)
266 |
267 | @mock.patch('requests.post', good_delete)
268 | def test_delete_rules_multiple(self):
269 | """ Delete multiple rules. """
270 | rules_list = [
271 | {"value": "Hello World"},
272 | {"value": "Hello", "tag": "mytag"}
273 | ]
274 | rules.delete_rules(rules_list,
275 | config_file_path=test_utils.test_config_path)
276 |
--------------------------------------------------------------------------------