Compare commits

..

19 Commits

Author SHA1 Message Date
Ben Lopatin c155c8a60d Cleanup for packaging 2016-04-07 17:41:54 -04:00
Ben Lopatin dd8d9562ff Remove Python 2.6 support
Not worth including if it poses *any* problems whatsoever
2016-04-07 17:08:00 -04:00
Ben Lopatin c6a43f0304 Add pypy, pypy3, and add Python 2.6 back to test matrix 2016-04-07 17:02:56 -04:00
Ben Lopatin ca52920690 Read README as setup.py long description
Previous code caused serious problems building package
2016-04-07 16:57:44 -04:00
Ben Lopatin db934494e2 Update documentation including README 2016-04-07 16:56:12 -04:00
Ben Lopatin 6dbbe74776 Switch fully to reStructuredText in README 2016-04-07 16:48:01 -04:00
Ben Lopatin 04bb9c550b Add Python 3.5, drop Python 2.6 2016-04-07 16:45:57 -04:00
Guillaume Beraudo 65e398343b Add sync_sso method 2016-01-19 18:06:03 +01:00
Guillaume Beraudo df1d274a33 Add sso_payload helper method 2016-01-19 18:06:03 +01:00
Guillaume Beraudo 9cc771f381 Fix pydiscoursecli option in README 2016-01-19 15:53:20 +01:00
Guillaume Beraudo 66cd4ab5de Add by_external_id method
Allow retrieving user information based on the external SSO id.
The returned internal id can be used with logout.
2016-01-19 15:53:20 +01:00
Guillaume Beraudo bd79423ba9 Send header to accept json responses
This is necessary for by_external method.
Otherwise, an error page is returned.
2016-01-19 15:53:20 +01:00
Guillaume Beraudo 65abb8119f Add log_out method 2016-01-19 15:53:20 +01:00
Guillaume Beraudo f0f3256e01 Fix python 3.4 support 2016-01-19 15:53:20 +01:00
Julia Grace ab00eb9cf7 Merge pull request #12 from tindie/added_method_private_msgs_unread
added method to get unread private messages
2015-04-22 11:27:00 -07:00
Julia Grace 3ccf20212e added method to get unread private messages 2015-04-22 11:22:51 -07:00
Julia Grace e008d1865e Merge pull request #9 from citadelgrad/master
Added two admin methods. Account suspension and user listing with search
2015-04-22 11:14:09 -07:00
Julia Grace 5506eacaeb Merge pull request #10 from tindie/fix_topic_test
fixed test_topic()'
2015-04-22 11:05:09 -07:00
Scott Nixon 05a58b1d62 Added two admin methods. Account suspension and user listing with search 2015-02-25 13:18:36 -08:00
20 changed files with 238 additions and 106 deletions
+10 -7
View File
@@ -1,11 +1,14 @@
sudo: false
language: python
python:
- "2.6"
- "2.7"
- "2.7"
- "3.4"
- "3.5"
- "pypy"
- "pypy3"
install:
- "pip install -r requirements.dev.txt"
- "[[ $TRAVIS_PYTHON_VERSION = '2.6' ]] && pip install unittest2 || echo"
- "pip install ."
install:
- "pip install -r requirements.dev.txt"
- "pip install ."
script: nosetests
script: python setup.py test
+58
View File
@@ -0,0 +1,58 @@
============
Contributing
============
For patches, please ensure that all existing tests pass, that you have adequate
tests added as necessary, and that all code is documented! The latter is
critical. If you add or update an existing function, class, or module, please
ensure you add a docstring or ensure the existing docstring is up-to-date.
Please use `Google docstring format
<http://sphinxcontrib-napoleon.readthedocs.org/en/latest/example_google.html>`_.
This *will* be enforced.
Testing
=======
The best way to run the tests is with `tox <http://tox.readthedocs.org/en/latest/>`_::
pip install tox
detox
Or it's slightly faster cousin `detox
<https://pypi.python.org/pypi/detox>`_ which will parallelize test runs::
pip install detox
detox
Alternatively, you can run the self test with the following commands::
pip install -r requirements.dev.txt
pip install -e .
python setup.py test
Live Testing
============
You can test against a Discourse instance by following the [Official Discourse developement instructions][discoursedev].
For the impatient here is the quick and dirty version::
git clone git@github.com:discourse/discourse.git
cd discourse
vagrant up
vagrant ssh
cd /vagrant
bundle install
bundle exec rake db:migrate
bundle exec rails s
Once running you can access the Discourse install at http://localhost:4000.
[discoursedev]: https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md "Discourse Vagrant"
TODO
====
Refer to, https://github.com/discourse/discourse_api/blob/master/routes.txt for
a list of all operations available in Discourse.
-30
View File
@@ -1,30 +0,0 @@
Development
------------
Refer to, https://github.com/discourse/discourse_api/blob/master/routes.txt for a list of all operations available in Discourse.
Unit tests
--------------
You can run the self test with the following commands::
pip install -r requirements.dev.txt
pip install -e .
nosetests
Live Testing
-----------------
You can test against a Discourse instance by following the [Official Discourse developement instructions][discoursedev].
For the impatient here is the quick and dirty version::
git clone git@github.com:discourse/discourse.git
cd discourse
vagrant up
vagrant ssh
cd /vagrant
bundle install
bundle exec rake db:migrate
bundle exec rails s
Once running you can access the Discourse install at http://localhost:4000.
[discoursedev]: https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md "Discourse Vagrant"
+17
View File
@@ -0,0 +1,17 @@
=========
Changelog
=========
0.2.0
=====
* Inital fork, including gberaudo's changes
* Packaging cleanup, dropping Python 2.6 support and adding Python 3.5, PyPy,
PyPy3
* Packaging on PyPI
0.1.0.dev
=========
All pre-PyPI development
+55
View File
@@ -0,0 +1,55 @@
.PHONY: clean-pyc clean-build docs clean
clean: clean-build clean-pyc clean-test-all
clean-build:
@rm -rf build/
@rm -rf dist/
@rm -rf *.egg-info
clean-pyc:
-@find . -name '*.pyc' -follow -print0 | xargs -0 rm -f &> /dev/null
-@find . -name '*.pyo' -follow -print0 | xargs -0 rm -f &> /dev/null
-@find . -name '__pycache__' -type d -follow -print0 | xargs -0 rm -rf &> /dev/null
clean-test:
rm -rf .coverage coverage*
rm -rf tests/.coverage test/coverage*
rm -rf htmlcov/
clean-test-all: clean-test
rm -rf .tox/
lint:
flake8 pydiscourse
test: ## Run test suite against current Python path
python setup.py test
test-coverage: clean-test
-py.test ${COVER_FLAGS} ${TEST_FLAGS}
@exit_code=$?
@-coverage html
@exit ${exit_code}
test-all: ## Run all tox test environments, parallelized
detox
check: clean-build clean-pyc clean-test lint test-coverage
release: clean ## Uploads new source and wheel distributions (cleans first)
python setup.py sdist upload
python setup.py bdist_wheel upload
dist: clean ## Creates new source and wheel distributions (cleans first)
python setup.py sdist
python setup.py bdist_wheel
ls -l dist
docs: ## Builds and open docs
$(MAKE) -C docs clean
$(MAKE) -C docs html
open docs/_build/html/index.html
help:
@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+23 -8
View File
@@ -1,15 +1,30 @@
===========
pydiscourse
------------
===========
A Python library for working with Discourse.
Its pretty basic right now but you need to start somewhere.
This is a fork of the original Tindie version. It was forked to include fixes,
additional functionality, and to distribute a package on PyPI.
Goals
=====
* Provide functional parity with the Discourse API, for the currently supported
version of Discourse (something of a moving target)
* Support all supported Python versions
* Document API
Examples
-----------
========
Create a client connection to a Discourse server::
from pydiscourse.client import DiscourseClient
client = DiscourseClient('http://example.com', api_username='username', api_key='areallylongstringfromdiscourse')
client = DiscourseClient(
'http://example.com',
api_username='username',
api_key='areallylongstringfromdiscourse')
Get info about a user::
@@ -34,11 +49,11 @@ Implement SSO for Discourse with your Python server::
return redirect('http://discuss.example.com' + url)
Command line
----------------
============
To help experiment with the Discourse API, pydiscourse provides a simple command line client::
export DISCOURSE_API_KEY=your_master_key
pydiscoursecli --host=http://yourhost --api-username=system latest_topics
pydiscoursecli --host=http://yourhost --api-username=system topics_by johnsmith
pydiscoursecli --host=http://yourhost --api-username=system user eviltrout
pydiscoursecli --host-http://yourhost --api-user-system latest_topics
pydiscoursecli --host-http://yourhost --api-user-system topics_by johnsmith
pydiscoursecli --host-http://yourhost --api-user-system user eviltrout
+2 -2
View File
@@ -53,9 +53,9 @@ copyright = u'2014, Marc Sibson'
# built documents.
#
# The short X.Y version.
version = '0.1'
version = '0.2.0'
# The full version, including alpha/beta/rc tags.
release = '0.1'
release = '0.2.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+3 -2
View File
@@ -1,4 +1,5 @@
Development
===============
.. include:: ../DEVELOP.md
:start-line: 2
.. include:: ../CONTRIBUTING.rst
:start-line: 3
+3 -2
View File
@@ -1,4 +1,5 @@
Introduction
==============
.. include:: ../README.md
:start-line: 2
.. include:: ../README.rst
:start-line: 3
+1 -1
View File
@@ -1 +1 @@
__version__ = '0.1.0.dev'
__version__ = '0.2.0'
+32 -3
View File
@@ -4,6 +4,7 @@ import logging
import requests
from pydiscourse.exceptions import DiscourseError, DiscourseServerError, DiscourseClientError
from pydiscourse.sso import sso_payload
log = logging.getLogger('pydiscourse.client')
@@ -32,9 +33,23 @@ class DiscourseClient(object):
return self._post('/users', name=name, username=username, email=email,
password=password, password_confirmation=confirmations, challenge=challenge, **kwargs)
def by_external_id(self, external_id):
response = self._get("/users/by-external/{0}".format(external_id))
return response['user']
def log_out(self, userid):
return self._post('/admin/users/{0}/log_out'.format(userid))
def trust_level(self, userid, level):
return self._put('/admin/users/{0}/trust_level'.format(userid), level=level)
def suspend(self, userid, duration, reason):
return self._put('/admin/users/{0}/suspend'.format(userid), duration=duration, reason=reason)
def list_users(self, type, **kwargs):
""" optional user search: filter='test@example.com' or filter='scott' """
return self._get('/admin/users/list/{0}.json'.format(type), **kwargs)
def update_avatar_from_url(self, username, url, **kwargs):
return self._post('/users/{0}/preferences/avatar'.format(username), file=url, **kwargs)
@@ -66,9 +81,14 @@ class DiscourseClient(object):
def set_preference(self, username=None, **kwargs):
if username is None:
username = self.api_username
return self._put(u'/users/{0}'.format(username), **kwargs)
def sync_sso(self, **kwargs):
# expect sso_secret, name, username, email, external_id, avatar_url, avatar_force_update
sso_secret = kwargs.pop('sso_secret')
payload = sso_payload(sso_secret, **kwargs)
return self._post('/admin/users/sync_sso?{0}'.format(payload), **kwargs)
def generate_api_key(self, userid, **kwargs):
return self._post('/admin/users/{0}/generate_api_key'.format(userid), **kwargs)
@@ -83,7 +103,7 @@ class DiscourseClient(object):
def users(self, filter=None, **kwargs):
if filter is None:
filter = 'active'
return self._get('/admin/users/list/{0}.json'.format(filter), **kwargs)
def private_messages(self, username=None, **kwargs):
@@ -91,6 +111,11 @@ class DiscourseClient(object):
username = self.api_username
return self._get('/topics/private-messages/{0}.json'.format(username), **kwargs)
def private_messages_unread(self, username=None, **kwargs):
if username is None:
username = self.api_username
return self._get('/topics/private-messages-unread/{0}.json'.format(username), **kwargs)
def hot_topics(self, **kwargs):
return self._get('/hot.json', **kwargs)
@@ -217,7 +242,11 @@ class DiscourseClient(object):
params['api_username'] = self.api_username
url = self.host + path
response = requests.request(verb, url, allow_redirects=False, params=params, timeout=self.timeout)
headers = {'Accept': 'application/json; charset=utf-8'}
response = requests.request(
verb, url, allow_redirects=False, params=params, headers=headers,
timeout=self.timeout)
log.debug('response %s: %s', response.status_code, repr(response.text))
if not response.ok:
+1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python
import cmd
import json
import optparse
+11 -8
View File
@@ -19,7 +19,7 @@ A SSO request handler might look something like
url = sso_redirect_url(nonce, SECRET, request.user.email, request.user.id, request.user.username)
return redirect('http://discuss.example.com' + url)
"""
import base64
from base64 import b64encode, b64decode
import hmac
import hashlib
@@ -50,11 +50,11 @@ def sso_validate(payload, signature, secret):
if not payload:
raise DiscourseError('Invalid payload..')
decoded = base64.decodestring(payload)
decoded = b64decode(payload.encode('utf-8')).decode('utf-8')
if 'nonce' not in decoded:
raise DiscourseError('Invalid payload..')
h = hmac.new(secret, payload, digestmod=hashlib.sha256)
h = hmac.new(secret.encode('utf-8'), payload.encode('utf-8'), digestmod=hashlib.sha256)
this_signature = h.hexdigest()
if this_signature != signature:
@@ -65,6 +65,13 @@ def sso_validate(payload, signature, secret):
return nonce
def sso_payload(secret, **kwargs):
return_payload = b64encode(urlencode(kwargs).encode('utf-8'))
h = hmac.new(secret.encode('utf-8'), return_payload, digestmod=hashlib.sha256)
query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()})
return query_string
def sso_redirect_url(nonce, secret, email, external_id, username, **kwargs):
"""
nonce: returned by sso_validate()
@@ -82,8 +89,4 @@ def sso_redirect_url(nonce, secret, email, external_id, username, **kwargs):
'username': username
})
return_payload = base64.encodestring(urlencode(kwargs))
h = hmac.new(secret, return_payload, digestmod=hashlib.sha256)
query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()})
return '/session/sso_login?%s' % query_string
return '/session/sso_login?%s' % sso_payload(secret, **kwargs)
-3
View File
@@ -1,3 +0,0 @@
-r requirements.txt
nose
mock
-1
View File
@@ -1 +0,0 @@
requests
+2
View File
@@ -0,0 +1,2 @@
[wheel]
universal = 1
+14 -29
View File
@@ -1,40 +1,23 @@
import codecs
import os
from setuptools import setup, find_packages
def read(fname):
return codecs.open(os.path.join(os.path.dirname(__file__), fname), 'rt').read()
# Provided as an attribute, so you can append to these instead
# of replicating them:
standard_exclude = ["*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak"]
standard_exclude_directories = [
".*", "CVS", "_darcs", "./build", "./dist", "EGG-INFO", "*.egg-info"
]
NAME = "pydiscourse"
DESCRIPTION = "A Python library for the Discourse API"
AUTHOR = "Marc Sibson"
AUTHOR_EMAIL = "sibson@gmail.com"
URL = "https://github.com/tindie/pydiscourse"
PACKAGE = "pydiscourse"
VERSION = __import__(PACKAGE).__version__
README = open('README.rst').read()
VERSION = __import__("pydiscourse").__version__
setup(
name=NAME,
name="pydiscourse",
version=VERSION,
description=DESCRIPTION,
long_description=read("README.md"),
author=AUTHOR,
author_email=AUTHOR_EMAIL,
description="A Python library for the Discourse API",
long_description=README,
author="Marc Sibson and contributors",
author_email="ben+pydiscourse@benlopatin.com",
license="BSD",
url=URL,
url="https://github.com/bennylope/pydiscourse",
packages=find_packages(exclude=["tests.*", "tests"]),
install_requires=['requests>=2.0.0'],
tests_require=['mock'],
test_suite='tests',
entry_points={
'console_scripts': [
'pydiscoursecli = pydiscourse.main:main'
@@ -47,8 +30,10 @@ setup(
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
'Programming Language :: Python :: Implementation :: PyPy',
],
zip_safe=False,
)
View File
+2 -2
View File
@@ -1,4 +1,4 @@
import base64
from base64 import b64decode
try: # py26
import unittest2 as unittest
@@ -65,7 +65,7 @@ class Test_sso_redirect_url(SSOTestCase):
sso.sso_validate(payload, params['sig'][0], self.secret)
# check the params have all the data we expect
payload = base64.decodestring(payload)
payload = b64decode(payload.encode('utf-8')).decode('utf-8')
payload = unquote(payload)
payload = dict((p.split('=') for p in payload.split('&')))
+4 -8
View File
@@ -1,11 +1,7 @@
[tox]
envlist = py26, py27, py34
envlist = py27, py34, py35, pypy, pypy3
[testenv]
deps=-rrequirements.dev.txt
commands=nosetests
[testenv:py26]
deps=
-rrequirements.dev.txt
unittest2
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/pydiscourse
commands = python setup.py test