Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c155c8a60d | |||
| dd8d9562ff | |||
| c6a43f0304 | |||
| ca52920690 | |||
| db934494e2 | |||
| 6dbbe74776 | |||
| 04bb9c550b | |||
| 65e398343b | |||
| df1d274a33 | |||
| 9cc771f381 | |||
| 66cd4ab5de | |||
| bd79423ba9 | |||
| 65abb8119f | |||
| f0f3256e01 | |||
| ab00eb9cf7 | |||
| 3ccf20212e | |||
| e008d1865e | |||
| 5506eacaeb | |||
| 05a58b1d62 |
+10
-7
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,5 @@
|
||||
Development
|
||||
===============
|
||||
.. include:: ../DEVELOP.md
|
||||
:start-line: 2
|
||||
|
||||
.. include:: ../CONTRIBUTING.rst
|
||||
:start-line: 3
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
Introduction
|
||||
==============
|
||||
.. include:: ../README.md
|
||||
:start-line: 2
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-line: 3
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.1.0.dev'
|
||||
__version__ = '0.2.0'
|
||||
|
||||
+32
-3
@@ -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,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import cmd
|
||||
import json
|
||||
import optparse
|
||||
|
||||
+11
-8
@@ -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)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
-r requirements.txt
|
||||
nose
|
||||
mock
|
||||
@@ -1 +0,0 @@
|
||||
requests
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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('&')))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user