Compare commits

..

14 Commits

Author SHA1 Message Date
Ben Lopatin 8304e7b2f5 Version bump 0.3.2 2016-04-17 11:36:09 -04:00
Ben Lopatin 5806beef34 Merge pull request #1 from danielzohar/master
Only return `nonce` from given payload
2016-04-17 11:32:41 -04:00
Daniel Zohar f905a957f4 Only return nonce from given payload 2016-04-17 16:00:35 +01:00
Ben Lopatin 06ca2c5a58 Update changelog to include in package description 2016-04-11 11:36:25 -04:00
Ben Lopatin bde4325776 Add autodoc generation 2016-04-08 18:45:57 -04:00
Ben Lopatin 6b7e570475 Update README
[ci skip]
2016-04-08 18:27:02 -04:00
Ben Lopatin 3659724f11 Remove duplicate requests dependency in tox config 2016-04-08 18:03:10 -04:00
Ben Lopatin 1e151fc51f Remove module import from setup.py
Get the version number from the file, not by importing and thus trying
to import requests
2016-04-08 17:57:19 -04:00
Ben Lopatin 63f120ddca Require requests for tests 2016-04-08 17:47:26 -04:00
Ben Lopatin 9cb96eaf76 Version bump 2016-04-08 17:38:16 -04:00
Ben Lopatin c5207759a8 Fix empty response handling 2016-04-08 17:36:18 -04:00
Ben Lopatin b14cd502ce Allow client import from top level module 2016-04-08 17:35:25 -04:00
Ben Lopatin 3a3bb843e5 Check for docstrings with flake8
[ci skip]
2016-04-08 13:59:18 -04:00
Ben Lopatin a2f961aebb Formatting cleanup and flake8 configuration
[ci skip]
2016-04-08 13:56:41 -04:00
15 changed files with 195 additions and 34 deletions
+4
View File
@@ -1,2 +1,6 @@
(Based on original authors list and may be incomplete)
Marc Sibson
James Potter
Ben Lopatin
Daniel Zohar
+17 -6
View File
@@ -1,14 +1,25 @@
=========
Changelog
=========
.. :changelog:
Release history
===============
0.3.2
-----
* SSO functionality fixes
0.3.1
-----
* Fix how empty responses are handled
0.3.0
=====
-----
* Added method to unsuspend suspended user
0.2.0
=====
-----
* Inital fork, including gberaudo's changes
* Packaging cleanup, dropping Python 2.6 support and adding Python 3.5, PyPy,
@@ -16,7 +27,7 @@ Changelog
* Packaging on PyPI
0.1.0.dev
=========
---------
All pre-PyPI development
+6
View File
@@ -0,0 +1,6 @@
include setup.py
include README.rst
include MANIFEST.in
include HISTORY.rst
include LICENSE
recursive-include pydiscourse
+6 -1
View File
@@ -46,9 +46,14 @@ dist: clean ## Creates new source and wheel distributions (cleans first)
python setup.py bdist_wheel
ls -l dist
docs: ## Builds and open docs
api-docs: ## Build autodocs from docstrings
sphinx-apidoc -f -o docs pydiscourse
manual-docs: ## Build written docs
$(MAKE) -C docs clean
$(MAKE) -C docs html
docs: api-docs manual-docs ## Builds and open docs
open docs/_build/html/index.html
help:
+13 -3
View File
@@ -14,17 +14,27 @@ additional functionality, and to distribute a package on PyPI.
Goals
=====
* Exceptional documentation
* Support all supported Python versions
* 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
The order here is important. The Discourse API is itself poorly documented so
the level of documentation in the Python client is critical.
Installation
============
::
pip install pydiscourse
Examples
========
Create a client connection to a Discourse server::
from pydiscourse.client import DiscourseClient
from pydiscourse import DiscourseClient
client = DiscourseClient(
'http://example.com',
api_username='username',
+2 -4
View File
@@ -28,9 +28,7 @@ import os
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -55,7 +53,7 @@ copyright = u'2014, Marc Sibson'
# The short X.Y version.
version = '0.3'
# The full version, including alpha/beta/rc tags.
release = '0.3.0'
release = '0.3.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+7
View File
@@ -0,0 +1,7 @@
pydiscourse
===========
.. toctree::
:maxdepth: 4
pydiscourse
+46
View File
@@ -0,0 +1,46 @@
pydiscourse package
===================
Submodules
----------
pydiscourse.client module
-------------------------
.. automodule:: pydiscourse.client
:members:
:undoc-members:
:show-inheritance:
pydiscourse.exceptions module
-----------------------------
.. automodule:: pydiscourse.exceptions
:members:
:undoc-members:
:show-inheritance:
pydiscourse.main module
-----------------------
.. automodule:: pydiscourse.main
:members:
:undoc-members:
:show-inheritance:
pydiscourse.sso module
----------------------
.. automodule:: pydiscourse.sso
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: pydiscourse
:members:
:undoc-members:
:show-inheritance:
+5 -1
View File
@@ -1 +1,5 @@
__version__ = '0.3.0'
# -*- coding: utf-8 -*-
__version__ = '0.3.2'
from pydiscourse.client import DiscourseClient
+12 -6
View File
@@ -127,7 +127,8 @@ class DiscourseClient(object):
????
"""
return self._put('/admin/users/{0}/suspend'.format(userid), duration=duration, reason=reason)
return self._put('/admin/users/{0}/suspend'.format(userid),
duration=duration, reason=reason)
def unsuspend(self, userid):
"""
@@ -251,7 +252,8 @@ class DiscourseClient(object):
Returns:
"""
return self._put('/users/{0}/preferences/username'.format(username), username=new_username, **kwargs)
return self._put('/users/{0}/preferences/username'.format(username),
username=new_username, **kwargs)
def set_preference(self, username=None, **kwargs):
"""
@@ -270,13 +272,15 @@ class DiscourseClient(object):
def sync_sso(self, **kwargs):
"""
expect sso_secret, name, username, email, external_id, avatar_url,
avatar_force_update
Args:
**kwargs:
Returns:
"""
# 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)
@@ -531,7 +535,8 @@ class DiscourseClient(object):
kwargs['term'] = term
return self._get('/search.json', **kwargs)
def create_category(self, name, color, text_color='FFFFFF', permissions=None, parent=None, **kwargs):
def create_category(self, name, color, text_color='FFFFFF',
permissions=None, parent=None, **kwargs):
"""
Args:
@@ -694,13 +699,14 @@ class DiscourseClient(object):
raise DiscourseServerError(msg, response=response)
if response.status_code == 302:
raise DiscourseError('Unexpected Redirect, invalid api key or host?', response=response)
raise DiscourseError(
'Unexpected Redirect, invalid api key or host?', response=response)
json_content = 'application/json; charset=utf-8'
content_type = response.headers['content-type']
if content_type != json_content:
# some calls return empty html documents
if response.content == ' ':
if not response.content.strip():
return None
raise DiscourseError('Invalid Response, expecting "{0}" got "{1}"'.format(
+1 -1
View File
@@ -31,7 +31,7 @@ class DiscourseCmd(cmd.Cmd):
try:
return method(*args, **kwargs)
except DiscourseError as e:
print (e, e.response.text)
print(e, e.response.text)
return e.response
return wrapper
+11 -7
View File
@@ -1,9 +1,11 @@
"""
Utilities to implement Single Sign On for Discourse with a Python managed authentication DB
Utilities to implement Single Sign On for Discourse with a Python managed
authentication DB
https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045
Thanks to James Potter for the heavy lifting, detailed at https://meta.discourse.org/t/sso-example-for-django/14258
Thanks to James Potter for the heavy lifting, detailed at
https://meta.discourse.org/t/sso-example-for-django/14258
A SSO request handler might look something like
@@ -16,7 +18,8 @@ A SSO request handler might look something like
except DiscourseError as e:
return HTTP400(e.args[0])
url = sso_redirect_url(nonce, SECRET, request.user.email, request.user.id, request.user.username)
url = sso_redirect_url(nonce, SECRET, request.user.email,
request.user.id, request.user.username)
return redirect('http://discuss.example.com' + url)
"""
from base64 import b64encode, b64decode
@@ -24,9 +27,10 @@ import hmac
import hashlib
try: # py3
from urllib.parse import unquote, urlencode
from urllib.parse import unquote, urlencode, parse_qs
except ImportError:
from urllib import unquote, urlencode
from urlparse import parse_qs
from pydiscourse.exceptions import DiscourseError
@@ -60,9 +64,9 @@ def sso_validate(payload, signature, secret):
if this_signature != signature:
raise DiscourseError('Payload does not match signature.')
nonce = decoded.split('=')[1]
return nonce
# Discourse returns querystring encoded value. We only need `nonce`
qs = parse_qs(decoded)
return qs['nonce'][0]
def sso_payload(secret, **kwargs):
+14 -4
View File
@@ -2,21 +2,31 @@ from setuptools import setup, find_packages
README = open('README.rst').read()
VERSION = __import__("pydiscourse").__version__
HISTORY = open('HISTORY.rst').read().replace('.. :changelog:', '')
with open("pydiscourse/__init__.py", "r") as module_file:
for line in module_file:
if line.startswith("__version__"):
version_string = line.split("=")[1]
VERSION = version_string.strip().replace("'", "")
setup(
name="pydiscourse",
version=VERSION,
description="A Python library for the Discourse API",
long_description=README,
long_description=README + '\n\n' + HISTORY,
author="Marc Sibson and contributors",
author_email="ben+pydiscourse@benlopatin.com",
license="BSD",
url="https://github.com/bennylope/pydiscourse",
packages=find_packages(exclude=["tests.*", "tests"]),
install_requires=['requests>=2.0.0'],
tests_require=['mock'],
install_requires=[
'requests>=2.0.0',
],
tests_require=[
'mock',
],
test_suite='tests',
entry_points={
'console_scripts': [
+37 -1
View File
@@ -1,8 +1,18 @@
import sys
import unittest
import mock
from pydiscourse import client
import sys
if sys.version_info < (3,):
def b(x):
return x
else:
import codecs
def b(x):
return codecs.latin_1_encode(x)[0]
def prepare_response(request):
# we need to mocked response to look a little more real
@@ -15,7 +25,7 @@ class ClientBaseTestCase(unittest.TestCase):
"""
def setUp(self):
self.host = 'testhost'
self.host = 'http://testhost'
self.api_username = 'testuser'
self.api_key = 'testkey'
@@ -35,6 +45,32 @@ class ClientBaseTestCase(unittest.TestCase):
self.assertEqual(kwargs, params)
class TestClientRequests(ClientBaseTestCase):
"""
Tests for common request handling
"""
@mock.patch('pydiscourse.client.requests')
def test_empty_content_http_ok(self, mocked_requests):
"""Empty content should not raise error
Critical to test against *bytestrings* rather than unicode
"""
mocked_response = mock.MagicMock()
mocked_response.content = b(' ')
mocked_response.status_code = 200
mocked_response.headers = {"content-type": "text/plain; charset=utf-8"}
assert "content-type" in mocked_response.headers
mocked_requests.request = mock.MagicMock()
mocked_requests.request.return_value = mocked_response
resp = self.client._request('GET', '/users/admin/1/unsuspend', {})
self.assertIsNone(resp)
@mock.patch('requests.request')
class TestUser(ClientBaseTestCase):
+14
View File
@@ -5,3 +5,17 @@ envlist = py27, py34, py35, pypy, pypy3
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/pydiscourse
commands = python setup.py test
[testenv:flake8]
basepython=python
deps=
flake8
flake8_docstrings
commands=
flake8 pydiscourse
[flake8]
ignore = E126,E128
max-line-length = 99
exclude = .ropeproject
max-complexity = 10