root commit

This commit is contained in:
David Marx
2018-04-14 18:55:03 -07:00
commit a27f690df4
5 changed files with 354 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2018, David Marx
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+157
View File
@@ -0,0 +1,157 @@
Python Pushshift.io API Wrapper (for comment/submission search)
===============================================================
.. _installation:
Installation
------------
.. code-block:: bash
pip install -e git+git://github.com/dmarx/psaw.git
At present, only python 3 is supported.
Description
-----------
A minimalist wrapper for searching public reddit comments/submissions via the pushshift.io API.
Pushshift is an extremely useful resource, but the API is poorly documented. As such, this API wrapper
is currently designed to make it easy to pass pretty much any search parameter the user wants to try.
Although it is not necessarily reflective of the current status of the API, I recommend you
attempt to familiarize yourself with the Pushshift API documentation to better understand what search
arguments are likely to work. The documentation is distributed across several locations:
* `API Documentaion on Google Docs <https://docs.google.com/document/d/171VdjT-QKJi6ul9xYJ4kmiHeC7t_3G31Ce8eozKp3VQ/edit>`_
* `API Documentation on github <https://github.com/pushshift/api>`_
* `/r/pushshift <https://www.reddit.com/r/pushshift/>`_
Features
--------
* Handles rate limiting and exponential backoff subject to maximum retries and
maximum backoff limits. A minimum rate limit of 1 request per second is used
as a default per consultation with Pushshift's maintainer,
`/u/Stuck_in_the_matrix <https://www.reddit.com/u/Stuck_in_the_matrix>`_.
* Handles paging of results. Returns all historical results for a given query by default.
* Returns results in ``comment`` and ``submission`` objects whose API is similar to the corresponding ``praw``
objects. Additionally, result objects have an additional ``.d_`` attribute that offers dict
access to the associated data attributes.
* Adds a ``created`` attribute which converts a comment/submission's ``created_utc`` timestamp
to the user's local time.
* Extremely simple interface to pass query arguments to the API. The API is sparsely documented,
so it's often fruitful to just try an argument and see if it works.
* Limited support for pushshift's ``aggs`` argument.
Demo usage
----------
.. code-block:: python
from psaw import PushshiftAPI
api = PushshiftAPI()
100 most recent submissions
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: python
# The `search_comments` and `search_submissions` methods return generator objects
gen = api.search_submissions(limit=100)
results = list(gen)
First 10 submissions to /r/politics in 2017, filtering results to url/author/title/subreddit fields.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``created_utc`` field will be added automatically (it's used for paging).
.. code-block:: python
list(api.search_submissions(after=int(dt.datetime(2017, 1, 1).timestamp()),
subreddit='politics',
filter=['url','author', 'title', 'subreddit'],
limit=10))
Trying a search argument that doesn't actually work
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
According to the pushshift.io API documentation, we should be able to search submissions by url,
but (at the time of this writing) this doesn't actually work in practice.
The API should still respect the ``limit`` argument and possibly other supported arguments,
but no guarantees. If you find that an argument you have passed is not supported by the API,
best thing is to just remove it from the query and modify your api call to only utilize
supported arguments to mitigate risks from of unexpected behavior.
.. code-block:: python
url = 'http://www.politico.com/story/2017/02/mike-flynn-russia-ties-investigation-235272'
url_results = list(api.search_submissions(url=url, limit=500))
len(url_results), any(r.url == url for r in url_results)
# 500, False
All AskReddit comments containing the text "OP"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Use the ``q`` parameter to search text. Omitting the ``limit`` parameter does a full
historical search. Requests are performed in batches of size specified by the
``max_results_per_request`` parameter (default=500). Omitting the "max_reponse_cache"
test in the demo below will return all results. Otherwise, this demo will perform two
API requests returning 500 comments each. Alternatively, the generator can be queried for additional results.
.. code-block:: python
gen = api.search_comments(q='OP', subreddit='askreddit')
max_response_cache = 1000
cache = []
for c in gen:
cache.append(c)
# Omit this test to actually return all results. Wouldn't recommend it though: could take a while, but you do you.
if len(cache) >= max_response_cache:
break
# If you really want to: pick up where we left off to get the rest of the results.
if False:
for c in gen:
cache.append(c)
Using the ``aggs`` argument to count comments mentioning trump each hour in past week
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Replicating the example from the pushshift documentation:
https://api.pushshift.io/reddit/search/comment/?q=trump&after=7d&aggs=created_utc&frequency=hour&size=0
I haven't really experimented much with this functionality of the API, so I figured
the simplest way to support it would be to just disable most of the bells and whistles
provided by the API wrapper when the ``aggs`` argument is provided (i.e. paging, converting
the result to a namedtuple for dot notation attribute access).
.. code-block:: python
api = PushshiftAPI()
gen = api.search_comments(q='trump',
after='7d',
aggs='created_utc',
frequency='hour',
size=0,
)
result = next(gen)
License
-------
PSAW's source is provided under the `Simplified BSD License
<https://github.com/dmarx/psaw/master/LICENSE>`_.
* Copyright (c), 2018, David Marx
+126
View File
@@ -0,0 +1,126 @@
from collections import namedtuple
import copy
import json
import requests
import time
from datetime import datetime as dt
class PushshiftAPIMinimal(object):
base_url = 'https://api.pushshift.io/reddit/{}/search/'
_limited_args = ('aggs')
def __init__(self,
max_retries=20,
max_sleep=3600,
backoff=2,
rate_limit=1,
max_results_per_request=500
):
assert rate_limit >=1
assert max_results_per_request <= 500
assert backoff >= 1
self.max_retries = max_retries
self.max_sleep = max_sleep
self.backoff = backoff
self.rate_limit = rate_limit
self.max_results_per_request = max_results_per_request
self._last_request_time = 0
self._utc_offset_secs = None
@property
def utc_offset_secs(self):
if not self._utc_offset_secs:
#self._utc_offset_secs = dt.datetime.utcnow().astimezone().utcoffset().total_seconds()
self._utc_offset_secs = dt.utcnow().astimezone().utcoffset().total_seconds()
return self._utc_offset_secs
def _limited(self, payload):
"""Turn off bells and whistles for special API endpoints"""
return any(arg in payload for arg in self._limited_args)
def _epoch_utc_to_local(self, epoch):
return epoch - self.utc_offset_secs
def _wrap_thing(self, thing, kind):
"""Mimic praw.Submission and praw.Comment API"""
thing['created'] = self._epoch_utc_to_local(thing['created_utc'])
thing['d_'] = copy.deepcopy(thing)
ThingType = namedtuple(kind, thing.keys())
thing = ThingType(**thing)
return thing
def _rate_limit(self, nth_request=0):
d = time.time() - self._last_request_time
interval = max(self.rate_limit, self.backoff*nth_request)
interval = min(interval, self.max_sleep)
if d < interval:
time.sleep(interval-d)
self._last_request_time = time.time()
def _add_nec_args(self, payload):
#if 'aggs' in payload:
if self._limited(payload):
# Do nothing I guess? Not sure how paging works on this endpoint...
return
if 'limit' not in payload:
payload['limit'] = self.max_results_per_request
if 'filter' in payload and payload.get('created_utc', None) is None:
if not isinstance(payload['filter'], list):
payload['filter'] = list(payload['filter'])
payload['filter'].append('created_utc')
def _get(self, kind, payload):
self._add_nec_args(payload)
url = self.base_url.format(kind)
i, success = 0, False
while (not success) and (i<self.max_retries):
self._rate_limit(i)
response = requests.get(url, params=payload)
success = response.status_code == 200
i+=1
response_json = json.loads(response.text)
outv = response_json['data']
#if 'aggs' in payload:
if self._limited(payload):
outv = response_json
return outv
def _query(self, kind, stop_condition=lambda **x: False, **kwargs):
limit = kwargs.get('limit', None)
payload = copy.deepcopy(kwargs)
n = 0
while True:
if limit is not None:
if limit > self.max_results_per_request:
payload['limit'] = self.max_results_per_request
limit -= self.max_results_per_request
else:
payload['limit'] = limit
limit = 0
results = self._get(kind, payload)
#if 'aggs' in payload:
if self._limited(payload):
yield results
return
if len(results) == 0:
return
for thing in results:
n+=1
if stop_condition(**thing):
return
thing = self._wrap_thing(thing, kind)
yield thing
payload['before'] = thing.created_utc
if (limit is not None) & (limit == 0):
return
def search_submissions(self, **kwargs):
return self._query(kind='submission', **kwargs)
def search_comments(self, **kwargs):
return self._query(kind='comment', **kwargs)
class PushshiftAPI(PushshiftAPIMinimal):
# Fill out this class with more user-friendly features later
pass
+9
View File
@@ -0,0 +1,9 @@
"""
Pushshift.io API Wrapper (for reddit.com public comment/submission search)
https://github.com/dmarx/psaw
"""
from .PushshiftAPI import PushshiftAPI, PushshiftAPIMinimal
__version__ = '0.0.1'
+40
View File
@@ -0,0 +1,40 @@
from setuptools import setup
from os import path
import re
PACKAGE_NAME='psaw'
HERE = path.abspath(path.dirname(__file__))
with open(path.join(HERE, 'README.md'), encoding='utf-8') as fp:
README = fp.read()
with open(path.join(HERE, PACKAGE_NAME, '__init__.py'),
encoding='utf-8') as fp:
VERSION = re.search("__version__ = '([^']+)'", fp.read()).group(1)
setup(name=PACKAGE_NAME,
packages=[PACKAGE_NAME],
version=VERSION,
long_description=README,
description='Pushshift.io API Wrapper for reddit.com public comment/submission search',
author='David Marx',
author_email='david.marx84@gmail.com',
url='http://github.com/dmarx/psaw',
license='Simplified BSD License',
install_requires=['requests'],
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Utilities']
)