Compare commits
No commits in common. "master" and "add-ruff" have entirely different histories.
3
.coveragerc
Normal file
3
.coveragerc
Normal file
@ -0,0 +1,3 @@
|
||||
[run]
|
||||
|
||||
include: pydiscourse/*
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.12"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
|
||||
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
13
HISTORY.rst
13
HISTORY.rst
@ -3,19 +3,6 @@
|
||||
Release history
|
||||
===============
|
||||
|
||||
|
||||
1.7.0
|
||||
-----
|
||||
|
||||
- Possible breaking change: Change `search()` term paramater from `term` to `q`,
|
||||
fixes search. Thanks @weber-s
|
||||
- Add support for Python 3.12
|
||||
|
||||
1.6.1
|
||||
-----
|
||||
|
||||
- Adds `posts_by_number` endpoint from @Dettorer
|
||||
|
||||
1.6.0
|
||||
-----
|
||||
|
||||
|
||||
70
README.rst
70
README.rst
@ -2,13 +2,9 @@
|
||||
pydiscourse
|
||||
===========
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pydiscourse?color=blue
|
||||
:alt: PyPI
|
||||
:target: https://pypi.org/project/pydiscourse/
|
||||
|
||||
.. image:: https://github.com/pydiscourse/pydiscourse/workflows/Tests/badge.svg
|
||||
.. image:: https://github.com/bennylope/pydiscourse/workflows/Tests/badge.svg
|
||||
:alt: Build Status
|
||||
:target: https://github.com/pydiscourse/pydiscourse/actions
|
||||
:target: https://github.com/bennylope/pydiscourse/actions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Check%20out%20the-Docs-blue.svg
|
||||
:alt: Check out the Docs
|
||||
@ -41,52 +37,42 @@ Installation
|
||||
Examples
|
||||
========
|
||||
|
||||
Create a client connection to a Discourse server:
|
||||
Create a client connection to a Discourse server::
|
||||
|
||||
.. code:: python
|
||||
from pydiscourse import DiscourseClient
|
||||
client = DiscourseClient(
|
||||
'http://example.com',
|
||||
api_username='username',
|
||||
api_key='areallylongstringfromdiscourse')
|
||||
|
||||
from pydiscourse import DiscourseClient
|
||||
client = DiscourseClient(
|
||||
'http://example.com',
|
||||
api_username='username',
|
||||
api_key='areallylongstringfromdiscourse')
|
||||
Get info about a user::
|
||||
|
||||
Get info about a user:
|
||||
user = client.user('eviltrout')
|
||||
print user
|
||||
|
||||
.. code:: python
|
||||
user_topics = client.topics_by('johnsmith')
|
||||
print user_topics
|
||||
|
||||
user = client.user('eviltrout')
|
||||
print user
|
||||
Create a new user::
|
||||
|
||||
user_topics = client.topics_by('johnsmith')
|
||||
print user_topics
|
||||
user = client.create_user('The Black Knight', 'blacknight', 'knight@python.org', 'justafleshwound')
|
||||
|
||||
Create a new user:
|
||||
Implement SSO for Discourse with your Python server::
|
||||
|
||||
.. code:: python
|
||||
|
||||
user = client.create_user('The Black Knight', 'blacknight', 'knight@python.org', 'justafleshwound')
|
||||
|
||||
Implement SSO for Discourse with your Python server:
|
||||
|
||||
.. code:: python
|
||||
|
||||
@login_required
|
||||
def discourse_sso_view(request):
|
||||
payload = request.GET.get('sso')
|
||||
signature = request.GET.get('sig')
|
||||
nonce = sso_validate(payload, signature, SECRET)
|
||||
url = sso_redirect_url(nonce, SECRET, request.user.email, request.user.id, request.user.username)
|
||||
return redirect('http://discuss.example.com' + url)
|
||||
@login_required
|
||||
def discourse_sso_view(request):
|
||||
payload = request.GET.get('sso')
|
||||
signature = request.GET.get('sig')
|
||||
nonce = sso_validate(payload, signature, SECRET)
|
||||
url = sso_redirect_url(nonce, SECRET, request.user.email, request.user.id, request.user.username)
|
||||
return redirect('http://discuss.example.com' + url)
|
||||
|
||||
Command line
|
||||
============
|
||||
|
||||
To help experiment with the Discourse API, pydiscourse provides a simple command line client:
|
||||
To help experiment with the Discourse API, pydiscourse provides a simple command line client::
|
||||
|
||||
.. code:: bash
|
||||
|
||||
export DISCOURSE_API_KEY=your_master_key
|
||||
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
|
||||
export DISCOURSE_API_KEY=your_master_key
|
||||
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
|
||||
|
||||
@ -51,9 +51,9 @@ copyright = u'2014, Marc Sibson'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.7'
|
||||
version = '1.6'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.7.0'
|
||||
release = '1.6.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
||||
@ -29,7 +29,6 @@ classifiers =
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
|
||||
[options.packages.find]
|
||||
where=src
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"""Python client for the Discourse API."""
|
||||
|
||||
__version__ = "1.7.0"
|
||||
__version__ = "1.6.0"
|
||||
|
||||
from pydiscourse.client import DiscourseClient
|
||||
|
||||
|
||||
@ -230,17 +230,6 @@ class DiscourseClient:
|
||||
"""
|
||||
return self._put(f"/admin/users/{userid}/trust_level", level=level)
|
||||
|
||||
def anonymize(self, userid):
|
||||
"""
|
||||
|
||||
Args:
|
||||
userid: the Discourse user ID
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return self._put(f"/admin/users/{userid}/anonymize")
|
||||
|
||||
def suspend(self, userid, duration, reason):
|
||||
"""
|
||||
Suspend a user's account
|
||||
@ -659,19 +648,6 @@ class DiscourseClient:
|
||||
"""
|
||||
return self._get(f"/posts/{post_id}.json", **kwargs)
|
||||
|
||||
def post_by_number(self, topic_id, post_number, **kwargs):
|
||||
"""
|
||||
Get a post from its number inside a specific topic
|
||||
Args:
|
||||
topic_id: the topic the post belongs to
|
||||
post_number: the number of the post inside the topic
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
post
|
||||
"""
|
||||
return self._get(f"/posts/by_number/{topic_id}/{post_number}", **kwargs)
|
||||
|
||||
def posts(self, topic_id, post_ids=None, **kwargs):
|
||||
"""
|
||||
Get a set of posts from a topic
|
||||
@ -855,17 +831,17 @@ class DiscourseClient:
|
||||
kwargs = {"email": user_email, "topic_id": topic_id}
|
||||
return self._post(f"/t/{topic_id}/invite.json", **kwargs)
|
||||
|
||||
def search(self, q, **kwargs):
|
||||
def search(self, term, **kwargs):
|
||||
"""
|
||||
|
||||
Args:
|
||||
q:
|
||||
term:
|
||||
**kwargs:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
kwargs["q"] = q
|
||||
kwargs["term"] = term
|
||||
return self._get("/search.json", **kwargs)
|
||||
|
||||
def badges(self, **kwargs):
|
||||
@ -1511,12 +1487,6 @@ class DiscourseClient:
|
||||
"""
|
||||
return self._post(f"/category/{category_id}/notifications", **kwargs)
|
||||
|
||||
def about(self):
|
||||
"""
|
||||
Get site info
|
||||
"""
|
||||
return self._get("/about.json")
|
||||
|
||||
def _get(self, path, override_request_kwargs=None, **kwargs):
|
||||
"""
|
||||
|
||||
|
||||
@ -5,14 +5,13 @@ import urllib.parse
|
||||
import pytest
|
||||
|
||||
|
||||
def test_empty_content_http_ok(discourse_host, discourse_client, discourse_request):
|
||||
def test_empty_content_http_ok(discourse_host, discourse_client, requests_mock):
|
||||
"""Empty content should not raise error
|
||||
|
||||
Critical to test against *bytestrings* rather than unicode
|
||||
"""
|
||||
discourse_request(
|
||||
"get",
|
||||
"/users/admin/1/unsuspend",
|
||||
requests_mock.get(
|
||||
f"{discourse_host}/users/admin/1/unsuspend",
|
||||
headers={"Content-Type": "text/plain; charset=utf-8"},
|
||||
content=b" ",
|
||||
)
|
||||
@ -33,18 +32,17 @@ class TestUserManagement:
|
||||
|
||||
assert request.called_once
|
||||
|
||||
def test_users(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/admin/users/list/active.json")
|
||||
discourse_client.users()
|
||||
assert request.called_once
|
||||
|
||||
def test_create_user(self, discourse_host, discourse_client, discourse_request):
|
||||
session_request = discourse_request(
|
||||
"get",
|
||||
"/session/hp.json",
|
||||
def test_create_user(self, discourse_host, discourse_client, requests_mock):
|
||||
session_request = requests_mock.get(
|
||||
f"{discourse_host}/session/hp.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={"challenge": "challenge", "value": "value"},
|
||||
)
|
||||
user_request = discourse_request("post", "/users")
|
||||
user_request = requests_mock.post(
|
||||
f"{discourse_host}/users",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.create_user(
|
||||
"Test User",
|
||||
"testuser",
|
||||
@ -83,12 +81,6 @@ class TestUserManagement:
|
||||
|
||||
assert request.called_once
|
||||
|
||||
def test_anonymize(self, discourse_client, discourse_request):
|
||||
request = discourse_request("put", "/admin/users/123/anonymize")
|
||||
discourse_client.anonymize(123)
|
||||
|
||||
assert request.called_once
|
||||
|
||||
@pytest.mark.usefixtures("_frozen_time")
|
||||
def test_suspend_user(self, discourse_client, discourse_request):
|
||||
request = discourse_request("put", "/admin/users/123/suspend")
|
||||
@ -116,39 +108,59 @@ class TestUserManagement:
|
||||
|
||||
|
||||
class TestTopics:
|
||||
def test_hot_topics(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/hot.json")
|
||||
def test_hot_topics(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/hot.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.hot_topics()
|
||||
assert request.called_once
|
||||
|
||||
def test_latest_topics(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/latest.json")
|
||||
def test_latest_topics(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/latest.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.latest_topics()
|
||||
|
||||
assert request.called_once
|
||||
|
||||
def test_new_topics(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/new.json")
|
||||
def test_new_topics(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/new.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.new_topics()
|
||||
assert request.called_once
|
||||
|
||||
def test_topic(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/t/some-test-slug/22.json")
|
||||
def test_topic(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/t/some-test-slug/22.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.topic("some-test-slug", 22)
|
||||
assert request.called_once
|
||||
|
||||
def test_topics_by(self, discourse_client, discourse_request):
|
||||
request = discourse_request(
|
||||
"get",
|
||||
"/topics/created-by/someuser.json",
|
||||
def test_topics_by(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/topics/created-by/someuser.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={"topic_list": {"topics": []}},
|
||||
)
|
||||
discourse_client.topics_by("someuser")
|
||||
|
||||
assert request.called_once
|
||||
|
||||
def test_invite_user_to_topic(self, discourse_client, discourse_request):
|
||||
request = discourse_request("post", "/t/22/invite.json")
|
||||
def test_invite_user_to_topic(self, discourse_client, requests_mock):
|
||||
request = requests_mock.post(
|
||||
f"{discourse_client.host}/t/22/invite.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.invite_user_to_topic("test@example.com", 22)
|
||||
assert request.called_once
|
||||
|
||||
@ -158,37 +170,40 @@ class TestTopics:
|
||||
assert request_payload["topic_id"] == ["22"]
|
||||
|
||||
|
||||
class TestPosts:
|
||||
def test_latest_posts(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/posts.json?before=54321")
|
||||
class TestEverything:
|
||||
def test_latest_posts(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/posts.json?before=54321",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.latest_posts(before=54321)
|
||||
assert request.called_once
|
||||
|
||||
def test_post_by_number(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/posts/by_number/8796/5")
|
||||
discourse_client.post_by_number(8796, 5)
|
||||
def test_search(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/search.json?term=needle",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.search(term="needle")
|
||||
assert request.called_once
|
||||
|
||||
|
||||
class TestSearch:
|
||||
def test_search(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/search.json?q=needle")
|
||||
discourse_client.search(q="needle")
|
||||
assert request.called_once
|
||||
|
||||
|
||||
class TestCategories:
|
||||
def test_categories(self, discourse_client, discourse_request):
|
||||
request = discourse_request(
|
||||
"get",
|
||||
"/categories.json",
|
||||
def test_categories(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/categories.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={"category_list": {"categories": []}},
|
||||
)
|
||||
discourse_client.categories()
|
||||
assert request.called_once
|
||||
|
||||
def test_update_category(self, discourse_client, discourse_request):
|
||||
request = discourse_request("put", "/categories/123")
|
||||
def test_update_category(self, discourse_client, requests_mock):
|
||||
request = requests_mock.put(
|
||||
f"{discourse_client.host}/categories/123",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.update_category(123, a="a", b="b")
|
||||
|
||||
request_payload = request.last_request.json()
|
||||
@ -196,25 +211,33 @@ class TestCategories:
|
||||
assert request_payload["a"] == "a"
|
||||
assert request_payload["b"] == "b"
|
||||
|
||||
def test_users(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/admin/users/list/active.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.users()
|
||||
assert request.called_once
|
||||
|
||||
class TestBadges:
|
||||
def test_badges(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/admin/badges.json")
|
||||
def test_badges(self, discourse_client, requests_mock):
|
||||
request = requests_mock.get(
|
||||
f"{discourse_client.host}/admin/badges.json",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.badges()
|
||||
assert request.called_once
|
||||
|
||||
def test_grant_badge_to(self, discourse_client, discourse_request):
|
||||
request = discourse_request("post", "/user_badges")
|
||||
def test_grant_badge_to(self, discourse_client, requests_mock):
|
||||
request = requests_mock.post(
|
||||
f"{discourse_client.host}/user_badges",
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
json={},
|
||||
)
|
||||
discourse_client.grant_badge_to("username", 1)
|
||||
|
||||
request_payload = urllib.parse.parse_qs(request.last_request.text)
|
||||
|
||||
assert request_payload["username"] == ["username"]
|
||||
assert request_payload["badge_id"] == ["1"]
|
||||
|
||||
|
||||
class TestAbout:
|
||||
def test_about(self, discourse_client, discourse_request):
|
||||
request = discourse_request("get", "/about.json")
|
||||
discourse_client.about()
|
||||
assert request.called_once
|
||||
|
||||
3
tox.ini
3
tox.ini
@ -7,14 +7,13 @@ python =
|
||||
3.9: py39
|
||||
3.10: py310
|
||||
3.11: py311
|
||||
3.12: py312
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
PYTHONPATH = {toxinidir}:{toxinidir}/pydiscourse
|
||||
commands =
|
||||
pytest {posargs} --cov=pydiscourse
|
||||
coverage report -m --include='**/pydiscourse/client.py' --fail-under=46
|
||||
coverage report -m --include='**/pydiscourse/client.py' --fail-under=45
|
||||
coverage report -m --include='**/pydiscourse/sso.py' --fail-under=100
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user