Compare commits

...

20 Commits

Author SHA1 Message Date
Samuël WEBER
6ee6fc8b88 docs: rename bennylope -> pydiscourse in readme 2025-02-25 11:33:51 -05:00
Samuël WEBER
7d05b1c42c docs: add pypi badge in readme 2025-02-25 11:33:51 -05:00
Samuël WEBER
b03c9738f1 docs: readme code highlighted 2025-02-25 11:33:29 -05:00
Ben Lopatin
36f8f1e737
Merge pull request #93 from pydiscourse/v1.7
Bump version
2024-04-16 16:34:57 -04:00
Ben Lopatin
2d27cc2a3c Update History 2024-04-16 16:32:22 -04:00
Ben Lopatin
f81c1b775a Add support for Python 3.12 2024-04-16 16:31:58 -04:00
Ben Lopatin
270f4bbbdb Bump version 2024-04-16 16:31:17 -04:00
Samuël WEBER
7d981684aa add anonymize endpoint 2024-02-24 13:26:43 -05:00
Samuël WEBER
53be265b23 fix: search term moved to q 2023-09-27 11:17:01 -04:00
Samuël WEBER
a07b122c39 add about endpoint for site statistics 2023-09-07 10:57:28 -04:00
Ben Lopatin
6d54153a24
Merge pull request #90 from pydiscourse/new-version
Update version for release
2023-09-01 10:37:25 -04:00
Ben Lopatin
c30a453a6b Bump version 2023-09-01 10:35:28 -04:00
Ben Lopatin
3e87ef3849
Merge pull request #89 from pydiscourse/use-discourse-request
Replace mocks and increase coverage requirement
2023-09-01 10:23:50 -04:00
Ben Lopatin
0f1cc41967 Bump up coverage requirement 2023-09-01 10:21:48 -04:00
Ben Lopatin
7ca7f47e94 Replace requests_mock with discourse_request
Requires just slightly less upfront configuration in each test.
2023-09-01 10:21:11 -04:00
Ben Lopatin
d8ec2f61aa
Merge pull request #88 from Dettorer/add-endpoint-post-by-number
Add a wrapper for the /posts/by_number endpoint
2023-09-01 10:08:46 -04:00
Paul Dettorer Hervot
f94d861c64 Add a wrapper for the /posts/by_number endpoint 2023-09-01 11:57:11 +02:00
Ben Lopatin
565b714d81
Merge pull request #87 from pydiscourse/bennylope-patch-1
Delete .coveragerc
2023-08-31 21:10:41 -04:00
Ben Lopatin
e1a9038789
Delete .coveragerc
Inapplicable now
2023-08-31 21:08:30 -04:00
Ben Lopatin
b38f99bffe
Merge pull request #86 from pydiscourse/add-ruff
Add ruff and cleanup code
2023-08-31 18:10:24 -04:00
10 changed files with 163 additions and 130 deletions

View File

@ -1,3 +0,0 @@
[run]
include: pydiscourse/*

View File

@ -13,7 +13,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.11"
python-version: "3.12"
- 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" ]
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ]
steps:
- uses: actions/checkout@v1

View File

@ -3,6 +3,19 @@
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
-----

View File

@ -2,9 +2,13 @@
pydiscourse
===========
.. image:: https://github.com/bennylope/pydiscourse/workflows/Tests/badge.svg
.. 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
:alt: Build Status
:target: https://github.com/bennylope/pydiscourse/actions
:target: https://github.com/pydiscourse/pydiscourse/actions
.. image:: https://img.shields.io/badge/Check%20out%20the-Docs-blue.svg
:alt: Check out the Docs
@ -37,42 +41,52 @@ Installation
Examples
========
Create a client connection to a Discourse server::
Create a client connection to a Discourse server:
from pydiscourse import DiscourseClient
client = DiscourseClient(
'http://example.com',
api_username='username',
api_key='areallylongstringfromdiscourse')
.. code:: python
Get info about a user::
from pydiscourse import DiscourseClient
client = DiscourseClient(
'http://example.com',
api_username='username',
api_key='areallylongstringfromdiscourse')
user = client.user('eviltrout')
print user
Get info about a user:
user_topics = client.topics_by('johnsmith')
print user_topics
.. code:: python
Create a new user::
user = client.user('eviltrout')
print user
user = client.create_user('The Black Knight', 'blacknight', 'knight@python.org', 'justafleshwound')
user_topics = client.topics_by('johnsmith')
print user_topics
Implement SSO for Discourse with your Python server::
Create a new user:
@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)
.. 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)
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:
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
.. 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

View File

@ -51,9 +51,9 @@ copyright = u'2014, Marc Sibson'
# built documents.
#
# The short X.Y version.
version = '1.6'
version = '1.7'
# The full version, including alpha/beta/rc tags.
release = '1.6.0'
release = '1.7.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -29,6 +29,7 @@ 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

View File

@ -1,6 +1,6 @@
"""Python client for the Discourse API."""
__version__ = "1.6.0"
__version__ = "1.7.0"
from pydiscourse.client import DiscourseClient

View File

@ -230,6 +230,17 @@ 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
@ -648,6 +659,19 @@ 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
@ -831,17 +855,17 @@ class DiscourseClient:
kwargs = {"email": user_email, "topic_id": topic_id}
return self._post(f"/t/{topic_id}/invite.json", **kwargs)
def search(self, term, **kwargs):
def search(self, q, **kwargs):
"""
Args:
term:
q:
**kwargs:
Returns:
"""
kwargs["term"] = term
kwargs["q"] = q
return self._get("/search.json", **kwargs)
def badges(self, **kwargs):
@ -1487,6 +1511,12 @@ 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):
"""

View File

@ -5,13 +5,14 @@ import urllib.parse
import pytest
def test_empty_content_http_ok(discourse_host, discourse_client, requests_mock):
def test_empty_content_http_ok(discourse_host, discourse_client, discourse_request):
"""Empty content should not raise error
Critical to test against *bytestrings* rather than unicode
"""
requests_mock.get(
f"{discourse_host}/users/admin/1/unsuspend",
discourse_request(
"get",
"/users/admin/1/unsuspend",
headers={"Content-Type": "text/plain; charset=utf-8"},
content=b" ",
)
@ -32,17 +33,18 @@ class TestUserManagement:
assert request.called_once
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"},
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",
json={"challenge": "challenge", "value": "value"},
)
user_request = requests_mock.post(
f"{discourse_host}/users",
headers={"Content-Type": "application/json; charset=utf-8"},
json={},
)
user_request = discourse_request("post", "/users")
discourse_client.create_user(
"Test User",
"testuser",
@ -81,6 +83,12 @@ 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")
@ -108,59 +116,39 @@ class TestUserManagement:
class TestTopics:
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={},
)
def test_hot_topics(self, discourse_client, discourse_request):
request = discourse_request("get", "/hot.json")
discourse_client.hot_topics()
assert request.called_once
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={},
)
def test_latest_topics(self, discourse_client, discourse_request):
request = discourse_request("get", "/latest.json")
discourse_client.latest_topics()
assert request.called_once
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={},
)
def test_new_topics(self, discourse_client, discourse_request):
request = discourse_request("get", "/new.json")
discourse_client.new_topics()
assert request.called_once
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={},
)
def test_topic(self, discourse_client, discourse_request):
request = discourse_request("get", "/t/some-test-slug/22.json")
discourse_client.topic("some-test-slug", 22)
assert request.called_once
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"},
def test_topics_by(self, discourse_client, discourse_request):
request = discourse_request(
"get",
"/topics/created-by/someuser.json",
json={"topic_list": {"topics": []}},
)
discourse_client.topics_by("someuser")
assert request.called_once
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={},
)
def test_invite_user_to_topic(self, discourse_client, discourse_request):
request = discourse_request("post", "/t/22/invite.json")
discourse_client.invite_user_to_topic("test@example.com", 22)
assert request.called_once
@ -170,40 +158,37 @@ class TestTopics:
assert request_payload["topic_id"] == ["22"]
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={},
)
class TestPosts:
def test_latest_posts(self, discourse_client, discourse_request):
request = discourse_request("get", "/posts.json?before=54321")
discourse_client.latest_posts(before=54321)
assert request.called_once
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")
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)
assert request.called_once
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"},
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",
json={"category_list": {"categories": []}},
)
discourse_client.categories()
assert request.called_once
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={},
)
def test_update_category(self, discourse_client, discourse_request):
request = discourse_request("put", "/categories/123")
discourse_client.update_category(123, a="a", b="b")
request_payload = request.last_request.json()
@ -211,33 +196,25 @@ class TestEverything:
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
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={},
)
class TestBadges:
def test_badges(self, discourse_client, discourse_request):
request = discourse_request("get", "/admin/badges.json")
discourse_client.badges()
assert request.called_once
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={},
)
def test_grant_badge_to(self, discourse_client, discourse_request):
request = discourse_request("post", "/user_badges")
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

View File

@ -7,13 +7,14 @@ 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=45
coverage report -m --include='**/pydiscourse/client.py' --fail-under=46
coverage report -m --include='**/pydiscourse/sso.py' --fail-under=100
deps =
-r{toxinidir}/requirements.txt