Skip to content
This repository was archived by the owner on Apr 5, 2025. It is now read-only.

Commit a1769f3

Browse files
committed
Added dynamic method generation
1 parent ac8c903 commit a1769f3

File tree

4 files changed

+148
-118
lines changed

4 files changed

+148
-118
lines changed

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,35 @@ To use BookStack's API, you'll need to get a token ID and secret.
1515

1616
You can find how to get these values from your BookStack instance's doc page at `http[s]://<example.com>/api/docs`
1717

18+
**Note**: Your account's user group must have API usage priveleges enabled.
19+
1820
# Usage
1921
Once you've acquired your token ID and secret, you're ready to rock.
2022

2123
```python
22-
import bookstack
24+
>>> import bookstack
2325

2426
# Input the appropriate values for these three variables
25-
base_url = 'http[s]://<example.com>'
26-
token_id = '<token_id>'
27-
token_secret = '<token_secret>'
27+
>>> base_url = 'http[s]://<example.com>'
28+
>>> token_id = '<token_id>'
29+
>>> token_secret = '<token_secret>'
30+
31+
>>> api = bookstack.BookStack(base_url, token_id=token_id, token_secret=token_secret)
32+
```
33+
34+
This wrapper *dynamically* generates its API calls at runtime. To have the wrapper generate the methods, use:
35+
36+
```python
37+
>>> api.generate_api_methods()
38+
39+
>>> api.available_api_methods
40+
{'get_books_export_pdf', 'get_shelves_list', 'post_books_create', 'get_docs_display', 'delete_shelves_delete', 'get_books_list', 'get_docs_json', 'delete_books_delete', 'get_books_read', 'get_shelves_read', 'put_books_update', 'get_books_export_plain_text', 'get_books_export_html', 'post_shelves_create', ...}
41+
```
2842

29-
api = bookstack.BookStack(base_url, token_id=token_id, token_secret=token_secret)
30-
```
43+
The above are then the methods available to you, for example:
44+
45+
```python
46+
>>> books_list = api.get_books_list()
47+
>>> books_list['data'][0]['name']
48+
'Mathematics'
49+
```

setup.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
from setuptools import setup, find_packages
22
import os
33

4-
5-
here = os.path.abspath(os.path.dirname(__file__))
6-
7-
about = {}
8-
with open(os.path.join(here, 'src', 'bookstack', '__init__.py'), 'r', encoding='utf-8') as f:
9-
exec(f.read(), about)
10-
11-
with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
12-
long_description = f.read()
13-
14-
install_requires = [
15-
'requests'
4+
CLASSIFIERS = [
5+
"Programming Language :: Python :: 3",
6+
"License :: OSI Approved :: MIT License",
7+
"Operating System :: OS Independent",
168
]
17-
18-
extras_require = {
9+
EXTRAS_REQUIRE = {
1910
'dev': [
2011
'bump2version',
2112
'pylint',
@@ -25,24 +16,43 @@
2516
'tox'
2617
]
2718
}
19+
INSTALL_REQUIRES = [
20+
'requests>= 2.18, < 3',
21+
'cached_property >= 1.5, < 2',
22+
'inflection == 0.4'
23+
]
24+
2825

26+
def get_about(here):
27+
about = {}
28+
about_info_path = os.path.join(here, 'src', 'bookstack', '__version__.py')
29+
with open(about_info_path,'r', encoding='utf-8' ) as f:
30+
exec(f.read(), about)
31+
32+
return about
33+
34+
def get_long_description(here):
35+
with open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
36+
long_description = f.read()
37+
38+
return long_description
39+
40+
41+
here = os.path.abspath(os.path.dirname(__file__))
42+
about = get_about(here)
2943

3044
setup(
3145
name=about['__title__'],
3246
version=about['__version__'],
3347
url=about['__url__'],
3448
description=about['__description__'],
35-
long_description=long_description,
49+
long_description=get_long_description(here),
3650
author=about['__author__'],
3751
author_email=about['__author_email__'],
3852
long_description_content_type='text/markdown',
3953
packages=find_packages('src'),
4054
package_dir={'': 'src'},
41-
install_requires=install_requires,
42-
extras_require=extras_require,
43-
classifiers=[
44-
"Programming Language :: Python :: 3",
45-
"License :: OSI Approved :: MIT License",
46-
"Operating System :: OS Independent",
47-
]
55+
install_requires=INSTALL_REQUIRES,
56+
extras_require=EXTRAS_REQUIRE,
57+
classifiers=CLASSIFIERS
4858
)

src/bookstack/models.py

Lines changed: 69 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,92 @@
1+
from json.decoder import JSONDecodeError
2+
import string
13
import urllib
24

5+
from cached_property import cached_property
6+
import inflection
37
import requests
48

59

10+
API_PATH = 'api/docs.json/'
11+
12+
613
class BookStack:
7-
def __init__(self, base_url, token_id=None, token_secret=None):
8-
self.api_base_url = self._create_api_base_url(base_url)
14+
def __init__(
15+
self,
16+
base_url,
17+
token_id=None,
18+
token_secret=None,
19+
api_path=API_PATH
20+
):
21+
self.api_base_url = base_url
922
self.token_id = None
1023
self.token_secret = None
24+
self._api_path = api_path
1125

26+
self.available_api_methods = set()
1227
self._session = BaseURLSession(self.api_base_url)
1328
self._session.auth = Auth(token_id, token_secret)
1429

15-
self.endpoint_paths = {
16-
'docs': 'docs.json/',
17-
'books': 'books/',
18-
'shelves': 'shelves/',
19-
'export_text': 'export/plaintext',
20-
'export_html': 'export/html'
21-
}
22-
23-
@staticmethod
24-
def _create_api_base_url(base_url):
25-
parsed_url = urllib.parse.urlparse(base_url)
26-
if 'api' in str(parsed_url.path):
27-
parsed_url = parsed_url
28-
else:
29-
parsed_url = parsed_url._replace(**{'path': 'api/'})
30-
31-
return parsed_url.geturl()
32-
33-
def get_docs(self):
34-
return self._session.get(self.endpoint_paths['docs']).json()
35-
36-
def get_books(self):
37-
return self._session.get(self.endpoint_paths['books']).json()
38-
39-
def read_book(self, book_id):
40-
return self._session.get(
41-
urllib.parse.urljoin(
42-
self.endpoint_paths['books'],
43-
str(book_id)
44-
)
45-
).json()
30+
def generate_api_methods(self):
31+
for base_model_info in self._get_api().values():
32+
for method_info in base_model_info:
33+
method_name = self._create_method_name(method_info)
4634

47-
def export_text(self, book_id):
48-
return self._session.get(
49-
urllib.parse.urljoin(
50-
self.endpoint_paths['books'],
51-
f"{book_id}/{self.endpoint_paths['export_text']}"
52-
)
53-
).text
35+
setattr(
36+
self,
37+
method_name,
38+
self._create_api_method(method_info)
39+
)
40+
41+
self.available_api_methods.add(method_name)
42+
43+
def _get_api(self):
44+
return self._session.request('GET', self._api_path).json()
5445

55-
def export_html(self, book_id):
56-
return self._session.get(
57-
urllib.parse.urljoin(
58-
self.endpoint_paths['books'],
59-
f"{book_id}/{self.endpoint_paths['export_html']}"
46+
def _create_api_method(self, method_info):
47+
def request_method(**kwargs):
48+
response = self._session.request(
49+
method_info['method'],
50+
method_info['uri'].format(**kwargs)
6051
)
61-
).text
62-
52+
53+
return self._get_response_content(response)
54+
55+
return request_method
56+
57+
@staticmethod
58+
def _get_api_args(uri):
59+
return [api_arg
60+
for _, api_arg, _, _ in string.Formatter().parse(uri)]
61+
62+
def _create_method_name(self, method_info):
63+
return self._format_camelcase(
64+
'_'.join([method_info['method'], method_info['name']])
65+
)
66+
67+
@staticmethod
68+
def _format_camelcase(string_):
69+
return inflection.underscore(string_)
70+
71+
@staticmethod
72+
def _get_response_content(response):
73+
try:
74+
content = response.json()
75+
except JSONDecodeError:
76+
content = response.text
77+
78+
return content
79+
6380

6481
class BaseURLSession(requests.Session):
6582
def __init__(self, base_url, *args, **kwargs):
6683
super().__init__(*args, **kwargs)
6784
self.base_url = base_url
6885

69-
def get(self, url_path, *args, **kwargs):
86+
def request(self, method, url_path, *args, **kwargs):
7087
url = urllib.parse.urljoin(self.base_url, url_path)
71-
return super().get(url, *args, **kwargs)
88+
89+
return super().request(method, url, *args, **kwargs)
7290

7391

7492
class Auth(requests.auth.AuthBase):
@@ -82,6 +100,7 @@ def __init__(self,
82100
self.token_secret = token_secret
83101

84102
def __call__(self, r):
85-
r.headers[self.header_key] = f'Token {self.token_id}:{self.token_secret}'
103+
r.headers[self.header_key] = \
104+
f'Token {self.token_id}:{self.token_secret}'
86105

87106
return r

tests/test_models.py

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -40,46 +40,42 @@ def test_intiialize(base_url, token_id, token_secret):
4040
assert token_id == token_id
4141
assert token_secret == token_secret
4242

43-
# @staticmethod
44-
# @pytest.mark.vcr()
45-
# def test_get_docs(fixture_bookstack):
46-
# test_result = fixture_bookstack._get_docs()
43+
@staticmethod
44+
def test_generate_api_methods(fixture_bookstack, fixture_api_info):
45+
fixture_bookstack.generate_api_methods()
4746

48-
# assert test_result
49-
# assert isinstance(test_result, dict)
50-
# assert all(key in test_result.keys() for key in ['docs'])
47+
assert 'get_books_list' in fixture_bookstack.available_api_methods
48+
assert 'get_books_list' in dir(fixture_bookstack)
5149

5250
@staticmethod
53-
# @pytest.mark.vcr()
54-
def test_api(fixture_bookstack):
55-
test_result = fixture_bookstack._api
51+
@pytest.mark.vcr()
52+
def test_get_docs_json(fixture_bookstack):
53+
test_result = fixture_bookstack.get_docs_json()
5654

57-
assert isinstance(test_result, bookstack.models.API)
55+
assert test_result
56+
assert isinstance(test_result, dict)
57+
assert all(key in test_result.keys() for key in ['docs'])
5858

5959
@staticmethod
60-
def test_methods(fixture_bookstack):
61-
test_result = fixture_bookstack.methods()
60+
def test_available_api_methods(fixture_bookstack):
61+
test_result = fixture_bookstack.available_api_methods
6262

63-
assert 'books_export_plain_text' in test_result
63+
assert 'get_books_export_plain_text' in test_result
6464
assert '_create_methods' not in test_result
6565

66-
@staticmethod
67-
def test_reset_api(fixture_bookstack):
68-
raise NotImplementedError
69-
7066
@staticmethod
7167
@pytest.mark.vcr()
72-
def test_get_books(fixture_bookstack):
73-
test_result = fixture_bookstack.get_books()
68+
def test_get_books_list(fixture_bookstack):
69+
test_result = fixture_bookstack.get_books_list()
7470

7571
assert test_result
7672
assert isinstance(test_result, dict)
7773
assert all(key in test_result.keys() for key in ['data', 'total'])
7874

7975
@staticmethod
8076
@pytest.mark.vcr()
81-
def test_read_book(fixture_bookstack):
82-
test_result = fixture_bookstack.read_book(1)
77+
def test_get_books_read(fixture_bookstack):
78+
test_result = fixture_bookstack.get_books_read(id=2)
8379

8480
assert test_result
8581
assert isinstance(test_result, dict)
@@ -88,30 +84,16 @@ def test_read_book(fixture_bookstack):
8884

8985
@staticmethod
9086
@pytest.mark.vcr()
91-
def test_export_text(fixture_bookstack):
92-
test_result = fixture_bookstack.export_text(1)
87+
def test_get_books_export_plain_text(fixture_bookstack):
88+
test_result = fixture_bookstack.get_books_export_plain_text(id=2)
9389

9490
assert test_result
9591
assert isinstance(test_result, str)
9692

9793
@staticmethod
9894
@pytest.mark.vcr()
99-
def test_export_html(fixture_bookstack):
100-
test_result = fixture_bookstack.export_html(1)
95+
def test_get_books_export_html(fixture_bookstack):
96+
test_result = fixture_bookstack.get_books_export_html(id=2)
10197

10298
assert test_result
10399
assert isinstance(test_result, str)
104-
105-
106-
class TestAPI:
107-
@staticmethod
108-
def test_intiialize_empty():
109-
with pytest.raises(TypeError):
110-
bookstack.models.API() # pylint: disable=no-value-for-parameter
111-
112-
@staticmethod
113-
def test_initialize(api_info):
114-
test_result = bookstack.models.API(api_info)
115-
116-
assert test_result
117-

0 commit comments

Comments
 (0)