Compare commits

..

73 Commits

Author SHA1 Message Date
Ben Lopatin 0d1eb1b816 Update history for release 2022-04-18 14:08:47 -04:00
Ben Lopatin 457abc559f Merge pull request #58 from Sebastian2023/fix_category
Fix getting category details
2022-04-18 14:03:31 -04:00
Sebastian Scherbel 3b7b7d2490 Fix getting category details 2022-04-18 14:02:25 -04:00
Ben Lopatin 1d74e2f1b7 Merge pull request #59 from Sebastian2023/add_site
Add option to fetch all categories including subcategories
2022-04-18 13:57:26 -04:00
Sebastian Scherbel e254926726 Add option to fetch all categories including subcategories 2022-04-18 09:33:03 +02:00
Ben Lopatin ff40a1c8c0 Correct license 2021-12-18 16:27:15 -05:00
Ben Lopatin 7054b9118f Merge pull request #54 from bennylope/python-updates
Update supported Python versions
2021-12-18 16:26:31 -05:00
Ben Lopatin b5ccf244a5 Ignore blame for moving to src, formatting 2021-12-18 16:24:31 -05:00
Ben Lopatin 901a53a10d Add missing console scripts 2021-12-18 16:22:12 -05:00
Ben Lopatin 71cb943e55 Update flake8 config 2021-12-18 16:18:05 -05:00
Ben Lopatin f6b4c02fc0 Format with black 2021-12-18 16:16:47 -05:00
Ben Lopatin 4eaff3a790 Update docstrings 2021-12-18 15:31:53 -05:00
Ben Lopatin 864b1b047f Add module docstring and __all__ definition 2021-12-18 15:31:03 -05:00
Ben Lopatin 69bdc5f76f Add and format docstrings 2021-12-18 15:26:15 -05:00
Ben Lopatin 27c76de371 Add docstrings 2021-12-18 15:25:20 -05:00
Ben Lopatin baaa049dc6 Fix flake8 path 2021-12-18 15:17:37 -05:00
Ben Lopatin bbe216ef8c Remove quotes from versions
Doesn't seem to be actually running tests...
2021-12-18 15:14:13 -05:00
Ben Lopatin d9a5c081a9 Use tox-gh-actions 2021-12-18 15:10:09 -05:00
Ben Lopatin f61ffdbcdb Install explicitly before testing 2021-12-18 15:06:47 -05:00
Ben Lopatin 8a0e742abd Update tests to use pytest 2021-12-18 15:05:43 -05:00
Ben Lopatin 1a22796e8e Use setup.cfg for metadata 2021-12-18 14:55:46 -05:00
Ben Lopatin 0177c46356 Move package into src directory 2021-12-18 14:55:46 -05:00
Ben Lopatin ef5f8523d8 Update tox environments 2021-12-18 14:42:50 -05:00
Ben Lopatin 96f9ea4b50 Remove Python 2 compatibility checks 2021-12-18 14:41:33 -05:00
Ben Lopatin 9b11c7d06a Use strings for version identifiers 2021-12-18 14:36:14 -05:00
Ben Lopatin 20c1915cbe Drop support for Python 2.7, 3.5, 3.6
Python 2.7 is EOL
Python 3.5 is EOL
Python 3.6 is EOL in 5 days
2021-12-18 14:31:45 -05:00
Ben Lopatin d5b9aacf01 Remove Travis config 2021-12-18 14:29:41 -05:00
Ben Lopatin bc8a2907b9 Merge pull request #53 from inducer/post-actions
Add post_action_users to see who liked a post
2021-12-18 14:28:11 -05:00
Andreas Kloeckner 1f595c3e7f Add post_action_users 2021-12-17 18:12:12 -06:00
Karl Goetz 188decb02a Merge pull request #50 from Ircam-Web/master
fix(errors): handle data-explorer responses
2021-02-12 15:32:35 +11:00
Martin Desrumaux f4bd3e3b17 fix(errors): handle data-explorer responses 2021-02-11 23:43:00 +01:00
Karl Goetz f74722dfb8 Merge pull request #49 from Ircam-Web/master
Implement new routes
2021-02-03 06:09:16 +11:00
Martin Desrumaux d101264391 fix(request): remove errors len check 2021-02-02 14:36:23 +01:00
Martin Desrumaux 3e94eaee05 fix(request): handle empty html documents 2021-01-22 14:12:20 +01:00
Martin Desrumaux 5763ba6ee8 docs(category): Add deprecation notice 2021-01-22 04:28:57 +01:00
Martin Desrumaux 099993a379 replace ' with " 2021-01-22 03:19:07 +01:00
Martin Desrumaux d887772b30 merge upstream 2021-01-22 03:11:09 +01:00
Raphaël Yancey e5d1ef2f02 Added notifications() method 2021-01-22 02:12:16 +01:00
Raphaël Voyazopoulos ee2769d0b9 Added category_latest_topics() method 2021-01-22 02:10:59 +01:00
Raphaël Voyazopoulos b2f6e1df96 Added delete_topic() method 2021-01-22 02:09:27 +01:00
Raphaël Voyazopoulos 0008bfdf0a Added top_topics() method 2021-01-22 02:09:27 +01:00
Raphaël Voyazopoulos 2eb6d672a0 Added user_by_id() method 2021-01-22 02:09:27 +01:00
Raphaël Voyazopoulos a68cb0244f Added data_explorer_query() method 2021-01-22 02:09:23 +01:00
Raphaël Voyazopoulos c0566f2aad Handling case where server returns empty errors prop 2021-01-22 02:07:15 +01:00
Raphaël Voyazopoulos fc6a78c948 Added user_by_email() method 2021-01-22 02:06:26 +01:00
Raphaël Voyazopoulos df30e1acc8 Added user_emails() method 2021-01-22 02:06:26 +01:00
Raphaël Voyazopoulos ec730ec026 Added empty response b'' as possible (not failed) responses 2021-01-22 02:06:23 +01:00
Raphaël Voyazopoulos 7fce4dc129 Added reset-bump-date endpoint 2021-01-21 18:05:31 +01:00
Raphaël Voyazopoulos 2bdfdb85ec Added get_site_settings() method 2021-01-21 18:05:28 +01:00
Raphaël Voyazopoulos f27ed47206 Added the delete_category method 2021-01-21 17:51:09 +01:00
Ben Lopatin cc9f35b5f3 Merge pull request #40 from kirstaylo/patch-1
Update client.py
2020-11-10 18:45:18 -05:00
Ben Lopatin 689e0981a0 Merge branch 'master' into patch-1 2020-11-10 18:39:37 -05:00
Ben Lopatin 712f9282b1 Version bump 1.1.2 2020-11-10 18:06:02 -05:00
Ben Lopatin b69a142811 Add blame ignore file 2020-11-10 18:03:57 -05:00
Ben Lopatin ce7038b05d Fix version and add history 2020-11-10 18:03:57 -05:00
Ben Lopatin ffbd47868d Merge pull request #47 from dkgv/master
Fix endpoint used for user creation (closes #46)
2020-11-10 17:26:53 -05:00
Gustav 5040b24dcc Fix endpoint used for user creation (closes #46)
See https://review.discourse.org/t/fix-move-hp-request-from-users-to-token-10795/15871
2020-11-10 13:26:48 +01:00
Ben Lopatin e7906a0568 Merge pull request #44 from dkgv/pypi-publishing
Add workflow to publish package to PyPI on tag/release (fixes #43)
2020-10-07 09:25:22 -04:00
Gustav 11a82695c5 Add workflow to publish package to PyPI on tag/release (fixes #43) 2020-10-04 22:16:42 +02:00
kirstaylo f1e7ee069c Update client.py
To avoid the error DiscourseClientError: param is missing or the value is empty: new_username
2020-08-31 12:39:04 +01:00
Alex Kerney 6e31953118 Fix 413 response from Discourse due to empty dict passed as json 2020-08-11 13:17:52 -04:00
Christian Kindel 2ad158e195 Fix call to get group info by name 2020-08-10 21:13:26 -04:00
Ben Lopatin 719035e9a9 Fix classifier error 2020-07-21 17:22:55 -04:00
Ben Lopatin 3a4af08827 Bump version 1.1.0 2020-07-21 17:21:10 -04:00
Alex Kerney 5d334f1d80 Use immutable arguments and adjust naming to simplify 2020-07-21 17:14:54 -04:00
Alex Kerney 361bf77949 Allow client methods to override requests kwargs
Due to some changes in the Discourse API, certain methods now work better if redirects are allowed.

Get /c/{id}.json will redirect to /c/{category_slug}.json which will cause the client.category_topics(id) method to fail as the redirect is not followed by default.

Now the keyword arguments to requests.request can be overridden by individual methods. This is implemented for .category_topics
2020-07-21 17:14:54 -04:00
Alex Kerney aeb763c42c Authenticate via headers
Closes bennylope/pydiscourse#27
2019-12-04 18:09:00 -05:00
Ben Lopatin 69867b3c10 Run test workflow on pull request 2019-10-13 18:30:40 -04:00
Ben Lopatin c3ae5b3c76 Update return documentation 2019-10-13 18:12:23 -04:00
Ben Lopatin d02ab15d3f Formatting 2019-10-13 18:10:12 -04:00
Richard Leyton 9a8641e596 Added invite() and invite_link() methods 2019-10-12 18:09:52 +01:00
Ben Lopatin 802f018519 Replace bad link with route documentation
Closes gh-173
2019-10-06 12:12:43 -04:00
Ben Lopatin be74c4e5b7 Add GitHub Actions testing workflow (#24)
* Add GitHub Actions testing workflow

* Update named Python versions and dev status

* Use unittest.mock as default mock source

Fall back to package for Python 2.7

* Try installing mock outside of setup

* Switch to GitHub actions shield
2019-10-06 12:04:51 -04:00
22 changed files with 458 additions and 155 deletions
+3
View File
@@ -0,0 +1,3 @@
c0db7215c95dbd31770ade1fc6ea65aa426d4590
0177c46356b9d0fc4b93f09aab7a224643a3685e
f6b4c02fc0f144dffc88cdd48b8261a69228d2f0
+33
View File
@@ -0,0 +1,33 @@
name: Publish package to PyPI
on:
push:
branches: [master]
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
- name: Build and publish distribution
if: (github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')) || github.event_name == 'release'
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
twine upload dist/*
+25
View File
@@ -0,0 +1,25 @@
name: Tests
on: [ push, pull_request ]
jobs:
test:
name: Test on Python ${{ matrix.py_version }}
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.7", "3.8", "3.9", "3.10" ]
steps:
- uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox
-11
View File
@@ -1,11 +0,0 @@
sudo: false
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
- "pypy3"
script: python setup.py test
+3 -1
View File
@@ -9,4 +9,6 @@ Scott Nixon
Jason Dorweiler
Pierre-Alain Dupont
Karl Goetz
Alex Kerney
Gustav <https://github.com/dkgv>
Sebastian2023 <https://github.com/Sebastian2023>
+5 -2
View File
@@ -69,5 +69,8 @@ Once running you can access the Discourse install at http://localhost:4000.
TODO
====
Refer to, https://github.com/discourse/discourse_api/blob/master/routes.txt for
a list of all operations available in Discourse.
For a list of all operations:
you can just run rake routes inside of the discourse repo to get an up to date list
Or check the old [`routes.txt`](https://github.com/discourse/discourse_api/blob/aa75df6cd851f0666f9e8071c4ef9dfdd39fc8f8/routes.txt) file, though this is certainly outdated.
+28
View File
@@ -3,6 +3,34 @@
Release history
===============
1.2.0
-----
- BREAKING? Dropped support for Python 2.7, 3.4, 3.5
- Added numerous new endpoint queries
- Updated category querying
1.1.2
-----
- Fix for Discourse users API change
1.1.1
-----
- Fix for empty dictionary and 413 API response
- Fix for getting member groups
1.1.0
-----
- Added ability to follow redirects in requests
1.0.0
-----
- Authenticate with headers
0.9.0
-----
+2 -2
View File
@@ -2,9 +2,9 @@
pydiscourse
===========
.. image:: https://secure.travis-ci.org/bennylope/pydiscourse.svg?branch=master
.. image:: https://github.com/bennylope/pydiscourse/workflows/Tests/badge.svg
:alt: Build Status
:target: http://travis-ci.org/bennylope/pydiscourse
:target: https://github.com/bennylope/pydiscourse/actions
.. image:: https://img.shields.io/badge/Check%20out%20the-Docs-blue.svg
:alt: Check out the Docs
+2 -2
View File
@@ -51,9 +51,9 @@ copyright = u'2014, Marc Sibson'
# built documents.
#
# The short X.Y version.
version = '0.9'
version = '1.1'
# The full version, including alpha/beta/rc tags.
release = '0.9.0'
release = '1.1.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
__version__ = "0.9.0"
from pydiscourse.client import DiscourseClient
-17
View File
@@ -1,17 +0,0 @@
from requests.exceptions import HTTPError
class DiscourseError(HTTPError):
""" A generic error while attempting to communicate with Discourse """
class DiscourseServerError(DiscourseError):
""" The Discourse Server encountered an error while processing the request """
class DiscourseClientError(DiscourseError):
""" An invalid request has been made """
class DiscourseRateLimitedError(DiscourseError):
""" Request required more than the permissible number of retries """
+2
View File
@@ -0,0 +1,2 @@
pytest==6.2.5
pytest-cov==3.0.0
+46 -1
View File
@@ -1,2 +1,47 @@
[wheel]
[metadata]
name = pydiscourse
version = attr: pydiscourse.__version__
author = "Marc Sibson and contributors"
author_email = "ben@benlopatin.com"
license = "MIT"
url = "https://github.com/bennylope/pydiscourse"
description = "A Python library for the Discourse API"
long_description = file: README.rst, HISTORY.rst
platforms =
OS Independent
[options]
zip_safe = False
include_package_data = True
packages = find:
package_dir =
=src
install_requires =
requests>=2.4.2
typing; python_version<"3.6"
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Web Environment
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
[options.packages.find]
where=src
[options.entry_points]
console_scripts =
pydiscoursecli = pydiscourse.main:main
[bdist_wheel]
universal = 1
[build-system]
requires =
setuptools >= "40.9.0"
wheel
+6 -48
View File
@@ -1,49 +1,7 @@
from setuptools import setup, find_packages
# -*- coding: utf-8 -*-
"""
See setup.cfg for packaging settings
"""
README = open('README.rst').read()
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 + '\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.4.2',
],
tests_require=[
'mock',
],
test_suite='tests',
entry_points={
'console_scripts': [
'pydiscoursecli = pydiscourse.main:main'
]
},
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
'Programming Language :: Python :: Implementation :: PyPy',
],
zip_safe=False,
)
from setuptools import setup
setup()
+10
View File
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
"""Python client for the Discourse API."""
__version__ = "1.2.0"
from pydiscourse.client import DiscourseClient
__all__ = ["DiscourseClient"]
@@ -84,6 +84,74 @@ class DiscourseClient(object):
"""
return self._get("/admin/users/{0}.json".format(user_id))
def invite(self, email, group_names, custom_message, **kwargs):
"""
Invite a user by email to join your forum
Args:
email: their email, will be used for activation and summary emails
group_names: the group names
custom_message: message to include
**kwargs: ???? what else can be sent through?
Returns:
API response body (dict)
"""
return self._post(
"/invites",
email=email,
group_names=group_names,
custom_message=custom_message,
**kwargs
)
def invite_link(self, email, group_names, custom_message, **kwargs):
"""
Generate an invite link for a user to join your forum
Args:
email: their email, will be used for activation and summary emails
group_names: the group names
custom_message: message to include
**kwargs: ???? what else can be sent through?
Returns:
Invite link
"""
return self._post(
"/invites/link",
email=email,
group_names=group_names,
custom_message=custom_message,
**kwargs
)
def user_by_id(self, pk):
"""
Get user from ID
Args:
pk: user id
Returns:
user
"""
return self._get("/admin/users/{0}.json".format(pk))
def user_by_email(self, email):
"""
Get user from email
Args:
email: user email
Returns:
user
"""
return self._get("/admin/users/list/all.json?email={0}".format(email))
def create_user(self, name, username, email, password, **kwargs):
"""
Create a Discourse user
@@ -103,7 +171,7 @@ class DiscourseClient(object):
????
"""
r = self._get("/users/hp.json")
r = self._get("/session/hp.json")
challenge = r["challenge"][::-1] # reverse challenge, discourse security check
confirmations = r["value"]
return self._post(
@@ -327,7 +395,7 @@ class DiscourseClient(object):
"""
return self._put(
"/users/{0}/preferences/username".format(username),
username=new_username,
new_username=new_username,
**kwargs
)
@@ -445,8 +513,14 @@ class DiscourseClient(object):
JSON API response
"""
return self._get("/c/{0}.json".format(category_id), **kwargs)
return self._get(
"/c/{0}.json".format(category_id),
override_request_kwargs={"allow_redirects": True},
**kwargs
)
# Doesn't work on recent Discourse versions (2014+)
# https://github.com/discourse/discourse_api/pull/204
def hot_topics(self, **kwargs):
"""
@@ -458,6 +532,15 @@ class DiscourseClient(object):
"""
return self._get("/hot.json", **kwargs)
def top_topics(self, **kwargs):
"""
Get top topics
Returns:
List of top topics
"""
return self._get("/top.json", **kwargs)
def latest_topics(self, **kwargs):
"""
@@ -520,6 +603,36 @@ class DiscourseClient(object):
"""
return self._get("/t/{0}/{1}.json".format(topic_id, post_id), **kwargs)
def post_action_users(self, post_id, post_action_type_id=None, **kwargs):
"""
Args:
post_id: int
post_action_type_id: Optional[int]
**kwargs:
Returns:
"""
# https://meta.discourse.org/t/getting-who-liked-a-post-from-the-api/103618
kwargs["id"] = post_id
if post_action_type_id is not None:
kwargs["post_action_type_id"] = post_action_type_id
return self._get("/post_action_users", **kwargs)
def post_by_id(self, post_id, **kwargs):
"""
Get a post from its id
Args:
post_id: id of the post
**kwargs:
Returns:
post
"""
return self._get("/posts/{0}.json".format(post_id), **kwargs)
def posts(self, topic_id, post_ids=None, **kwargs):
"""
Get a set of posts from a topic
@@ -648,6 +761,14 @@ class DiscourseClient(object):
kwargs["post[edit_reason]"] = edit_reason
return self._put("/posts/{0}".format(post_id), **kwargs)
def reset_bump_date(self, topic_id, **kwargs):
"""
Reset bump date
See https://meta.discourse.org/t/what-is-a-bump/105562
"""
return self._put("/t/{0}/reset-bump-date".format(topic_id), **kwargs)
def topics_by(self, username, **kwargs):
"""
@@ -788,21 +909,18 @@ class DiscourseClient(object):
"""
return self._get("/categories.json", **kwargs)["category_list"]["categories"]
def category(self, name, parent=None, **kwargs):
def category(self, category_id, parent=None, **kwargs):
"""
Args:
name:
parent:
category_id:
**kwargs:
Returns:
"""
if parent:
name = u"{0}/{1}".format(parent, name)
return self._get(u"/category/{0}.json".format(name), **kwargs)
return self._get(u"/c/{0}/show.json".format(category_id), **kwargs)
def delete_category(self, category_id, **kwargs):
"""
@@ -817,12 +935,32 @@ class DiscourseClient(object):
"""
return self._delete(u"/categories/{0}".format(category_id), **kwargs)
def get_site_info(self):
"""
Get site info to fetch all categories and subcategories
"""
return self._get("/site.json")
def get_site_settings(self):
"""
Get site settings
"""
return self._get("/admin/site_settings.json")
def category_latest_topics(self, name, parent=None, **kwargs):
"""
Get latest topics from a category
"""
if parent:
name = u"{0}/{1}".format(parent, name)
return self._get(u"/c/{0}/l/latest.json".format(name), **kwargs)
def site_settings(self, **kwargs):
"""
Update site settings
Args:
settings:
**kwargs:
**kwargs: key-value of properties to update
Returns:
@@ -901,7 +1039,7 @@ class DiscourseClient(object):
"""
Get all infos of a group by group name
"""
return self._get("/groups/{0}/members.json".format(group_name))
return self._get("/groups/{0}.json".format(group_name))
def create_group(
self,
@@ -1237,7 +1375,29 @@ class DiscourseClient(object):
kwargs["parent_tag_name"] = parent_tag_name
return self._post("/tag_groups", json=True, **kwargs)["tag_group"]
def _get(self, path, **kwargs):
def data_explorer_query(self, query_id, **kwargs):
"""
Run a query with database explorer plugin.
Requires discourse-data-explorer installed
https://github.com/discourse/discourse-data-explorer
"""
return self._post(
"/admin/plugins/explorer/queries/{}/run".format(query_id), **kwargs
)
def notifications(self, category_id, **kwargs):
"""
Get notifications
Args:
category_id
**kwargs:
notification_level=(int)
"""
return self._post("/category/{}/notifications".format(category_id), **kwargs)
def _get(self, path, override_request_kwargs=None, **kwargs):
"""
Args:
@@ -1247,9 +1407,11 @@ class DiscourseClient(object):
Returns:
"""
return self._request(GET, path, params=kwargs)
return self._request(
GET, path, params=kwargs, override_request_kwargs=override_request_kwargs
)
def _put(self, path, json=False, **kwargs):
def _put(self, path, json=False, override_request_kwargs=None, **kwargs):
"""
Args:
@@ -1260,12 +1422,18 @@ class DiscourseClient(object):
"""
if not json:
return self._request(PUT, path, data=kwargs)
return self._request(
PUT, path, data=kwargs, override_request_kwargs=override_request_kwargs
)
else:
return self._request(PUT, path, json=kwargs)
return self._request(
PUT, path, json=kwargs, override_request_kwargs=override_request_kwargs
)
def _post(self, path, files={}, json=False, **kwargs):
def _post(
self, path, files=None, json=False, override_request_kwargs=None, **kwargs
):
"""
Args:
@@ -1276,12 +1444,24 @@ class DiscourseClient(object):
"""
if not json:
return self._request(POST, path, files=files, data=kwargs)
return self._request(
POST,
path,
files=files,
data=kwargs,
override_request_kwargs=override_request_kwargs,
)
else:
return self._request(POST, path, files=files, json=kwargs)
return self._request(
POST,
path,
files=files,
json=kwargs,
override_request_kwargs=override_request_kwargs,
)
def _delete(self, path, **kwargs):
def _delete(self, path, override_request_kwargs=None, **kwargs):
"""
Args:
@@ -1291,9 +1471,20 @@ class DiscourseClient(object):
Returns:
"""
return self._request(DELETE, path, params=kwargs)
return self._request(
DELETE, path, params=kwargs, override_request_kwargs=override_request_kwargs
)
def _request(self, verb, path, params={}, files={}, data={}, json={}):
def _request(
self,
verb,
path,
params=None,
files=None,
data=None,
json=None,
override_request_kwargs=None,
):
"""
Executes HTTP request to API and handles response
@@ -1301,16 +1492,22 @@ class DiscourseClient(object):
verb: HTTP verb as string: GET, DELETE, PUT, POST
path: the path on the Discourse API
params: dictionary of parameters to include to the API
override_request_kwargs: dictionary of requests.request
keyword arguments to override defaults
Returns:
dictionary of response body data or None
"""
params["api_key"] = self.api_key
if "api_username" not in params:
params["api_username"] = self.api_username
override_request_kwargs = override_request_kwargs or {}
url = self.host + path
headers = {"Accept": "application/json; charset=utf-8"}
headers = {
"Accept": "application/json; charset=utf-8",
"Api-Key": self.api_key,
"Api-Username": self.api_username,
}
# How many times should we retry if rate limited
retry_count = 4
@@ -1318,9 +1515,7 @@ class DiscourseClient(object):
retry_backoff = 1
while retry_count > 0:
response = requests.request(
verb,
url,
request_kwargs = dict(
allow_redirects=False,
params=params,
files=files,
@@ -1330,6 +1525,10 @@ class DiscourseClient(object):
timeout=self.timeout,
)
request_kwargs.update(override_request_kwargs)
response = requests.request(verb, url, **request_kwargs)
log.debug("response %s: %s", response.status_code, repr(response.text))
if response.ok:
break
@@ -1396,7 +1595,10 @@ class DiscourseClient(object):
except ValueError:
raise DiscourseError("failed to decode response", response=response)
if "errors" in decoded:
# Checking "errors" length because
# data-explorer (e.g. POST /admin/plugins/explorer/queries/{}/run)
# sends an empty errors array
if "errors" in decoded and len(decoded["errors"]) > 0:
message = decoded.get("message")
if not message:
message = u",".join(decoded["errors"])
+19
View File
@@ -0,0 +1,19 @@
"""API exceptions."""
from requests.exceptions import HTTPError
class DiscourseError(HTTPError):
"""A generic error while attempting to communicate with Discourse"""
class DiscourseServerError(DiscourseError):
"""The Discourse Server encountered an error while processing the request"""
class DiscourseClientError(DiscourseError):
"""An invalid request has been made"""
class DiscourseRateLimitedError(DiscourseError):
"""Request required more than the permissible number of retries"""
@@ -1,5 +1,7 @@
#!/usr/bin/env python
"""Simple command line interface for making Discourse API queries."""
import cmd
import json
import logging
@@ -12,15 +14,19 @@ from pydiscourse.client import DiscourseClient, DiscourseError
class DiscourseCmd(cmd.Cmd):
"""Handles CLI commands"""
prompt = "discourse>"
output = sys.stdout
def __init__(self, client):
"""Initialize command"""
cmd.Cmd.__init__(self)
self.client = client
self.prompt = "%s>" % self.client.host
def __getattr__(self, attr):
"""Gets attributes with dynamic name handling"""
if attr.startswith("do_"):
method = getattr(self.client, attr[3:])
@@ -48,6 +54,7 @@ class DiscourseCmd(cmd.Cmd):
raise AttributeError
def postcmd(self, result, line):
"""Writes output of the command to console"""
try:
json.dump(
result, self.output, sort_keys=True, indent=4, separators=(",", ": ")
@@ -57,6 +64,7 @@ class DiscourseCmd(cmd.Cmd):
def main():
"""Runs the CLI application"""
op = optparse.OptionParser()
op.add_option("--host", default="http://localhost:4000")
op.add_option("--api-user", default="system")
+11 -13
View File
@@ -1,6 +1,4 @@
"""
Utilities to implement Single Sign On for Discourse with a Python managed
authentication DB
"""Implement Single Sign On for Discourse with a Python managed auth DB.
https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045
@@ -26,23 +24,20 @@ from base64 import b64encode, b64decode
import hmac
import hashlib
try: # py3
from urllib.parse import unquote, urlencode, parse_qs
except ImportError:
from urllib import unquote, urlencode
from urlparse import parse_qs
from urllib.parse import unquote, urlencode, parse_qs
from pydiscourse.exceptions import DiscourseError
def sso_validate(payload, signature, secret):
"""
"""Validates SSO payload.
Args:
payload: provided by Discourse HTTP call to your SSO endpoint as sso GET param
signature: provided by Discourse HTTP call to your SSO endpoint as sig GET param
secret: the secret key you entered into Discourse sso secret
return value: The nonce used by discourse to validate the redirect URL
return value: The nonce used by discourse to validate the redirect URL
"""
if None in [payload, signature]:
raise DiscourseError("No SSO payload or signature.")
@@ -72,6 +67,7 @@ def sso_validate(payload, signature, secret):
def sso_payload(secret, **kwargs):
"""Returns an encoded SSO payload"""
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()})
@@ -79,14 +75,16 @@ def sso_payload(secret, **kwargs):
def sso_redirect_url(nonce, secret, email, external_id, username, **kwargs):
"""
"""Returns the Discourse redirection URL.
Args:
nonce: returned by sso_validate()
secret: the secret key you entered into Discourse sso secret
user_email: email address of the user who logged in
user_id: the internal id of the logged in user
user_username: username of the logged in user
return value: URL to redirect users back to discourse, now logged in as user_username
return value: URL to redirect users back to discourse, now logged in as user_username
"""
kwargs.update(
{
+6 -6
View File
@@ -1,10 +1,10 @@
import sys
import unittest
import mock
from unittest import mock
from pydiscourse import client
import sys
if sys.version_info < (3,):
@@ -46,12 +46,12 @@ class ClientBaseTestCase(unittest.TestCase):
self.assertEqual(args[0], verb)
self.assertEqual(args[1], self.host + url)
kwargs = kwargs["params"]
self.assertEqual(kwargs.pop("api_username"), self.api_username)
self.assertEqual(kwargs.pop("api_key"), self.api_key)
headers = kwargs["headers"]
self.assertEqual(headers.pop("Api-Username"), self.api_username)
self.assertEqual(headers.pop("Api-Key"), self.api_key)
if verb == "GET":
self.assertEqual(kwargs, params)
self.assertEqual(kwargs["params"], params)
class TestClientRequests(ClientBaseTestCase):
+3 -12
View File
@@ -1,18 +1,9 @@
from base64 import b64decode
try: # py26
import unittest2 as unittest
except ImportError:
import unittest
try: # py3
from urllib.parse import unquote
from urllib.parse import urlparse, parse_qs
except ImportError:
from urlparse import urlparse, parse_qs
from urllib import unquote
import unittest
from urllib.parse import unquote
from urllib.parse import urlparse, parse_qs
from pydiscourse import sso
from pydiscourse.exceptions import DiscourseError
+13 -4
View File
@@ -1,10 +1,19 @@
[tox]
envlist = py27, py34, py35, py36, py37, pypy, pypy3
envlist = py37, py38, py39, py310
[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
3.10: py310
[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/pydiscourse
commands = python setup.py test
commands = pytest {posargs} --cov=pydiscourse
deps =
-r{toxinidir}/requirements.txt
[testenv:flake8]
basepython=python
@@ -12,10 +21,10 @@ deps=
flake8
flake8_docstrings
commands=
flake8 pydiscourse
flake8 src/pydiscourse --docstring-convention google --ignore D415
[flake8]
ignore = E126,E128
max-line-length = 99
max-line-length = 119
exclude = .ropeproject
max-complexity = 10