klarient-listmonk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. klarient_listmonk-0.1.0/.github/workflows/python-publish.yml +69 -0
  2. klarient_listmonk-0.1.0/.gitignore +158 -0
  3. klarient_listmonk-0.1.0/PKG-INFO +365 -0
  4. klarient_listmonk-0.1.0/README.md +338 -0
  5. klarient_listmonk-0.1.0/examples/bounces.py +69 -0
  6. klarient_listmonk-0.1.0/examples/campaigns.py +79 -0
  7. klarient_listmonk-0.1.0/examples/common.py +54 -0
  8. klarient_listmonk-0.1.0/examples/imports.py +59 -0
  9. klarient_listmonk-0.1.0/examples/lists.py +87 -0
  10. klarient_listmonk-0.1.0/examples/media.py +46 -0
  11. klarient_listmonk-0.1.0/examples/public.py +50 -0
  12. klarient_listmonk-0.1.0/examples/settings.example.json +16 -0
  13. klarient_listmonk-0.1.0/examples/subscribers.py +217 -0
  14. klarient_listmonk-0.1.0/examples/templates.py +99 -0
  15. klarient_listmonk-0.1.0/examples/transactional.py +52 -0
  16. klarient_listmonk-0.1.0/pyproject.toml +69 -0
  17. klarient_listmonk-0.1.0/setup.cfg +4 -0
  18. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/PKG-INFO +365 -0
  19. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/SOURCES.txt +69 -0
  20. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/dependency_links.txt +1 -0
  21. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/requires.txt +8 -0
  22. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/scm_file_list.json +66 -0
  23. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/scm_version.json +8 -0
  24. klarient_listmonk-0.1.0/src/klarient_listmonk.egg-info/top_level.txt +1 -0
  25. klarient_listmonk-0.1.0/src/listmonk/__init__.py +5 -0
  26. klarient_listmonk-0.1.0/src/listmonk/bounces/__init__.py +27 -0
  27. klarient_listmonk-0.1.0/src/listmonk/bounces/requests.py +101 -0
  28. klarient_listmonk-0.1.0/src/listmonk/bounces/resources.py +36 -0
  29. klarient_listmonk-0.1.0/src/listmonk/bounces/responses.py +92 -0
  30. klarient_listmonk-0.1.0/src/listmonk/campaigns/__init__.py +2 -0
  31. klarient_listmonk-0.1.0/src/listmonk/campaigns/requests.py +383 -0
  32. klarient_listmonk-0.1.0/src/listmonk/campaigns/resources.py +147 -0
  33. klarient_listmonk-0.1.0/src/listmonk/campaigns/responses.py +263 -0
  34. klarient_listmonk-0.1.0/src/listmonk/client.py +57 -0
  35. klarient_listmonk-0.1.0/src/listmonk/common.py +49 -0
  36. klarient_listmonk-0.1.0/src/listmonk/imports/__init__.py +25 -0
  37. klarient_listmonk-0.1.0/src/listmonk/imports/requests.py +134 -0
  38. klarient_listmonk-0.1.0/src/listmonk/imports/resources.py +39 -0
  39. klarient_listmonk-0.1.0/src/listmonk/imports/responses.py +54 -0
  40. klarient_listmonk-0.1.0/src/listmonk/lists/__init__.py +32 -0
  41. klarient_listmonk-0.1.0/src/listmonk/lists/requests.py +169 -0
  42. klarient_listmonk-0.1.0/src/listmonk/lists/resources.py +47 -0
  43. klarient_listmonk-0.1.0/src/listmonk/lists/responses.py +107 -0
  44. klarient_listmonk-0.1.0/src/listmonk/media/__init__.py +13 -0
  45. klarient_listmonk-0.1.0/src/listmonk/media/requests.py +70 -0
  46. klarient_listmonk-0.1.0/src/listmonk/media/resources.py +26 -0
  47. klarient_listmonk-0.1.0/src/listmonk/media/responses.py +74 -0
  48. klarient_listmonk-0.1.0/src/listmonk/paging.py +70 -0
  49. klarient_listmonk-0.1.0/src/listmonk/public/__init__.py +14 -0
  50. klarient_listmonk-0.1.0/src/listmonk/public/requests.py +67 -0
  51. klarient_listmonk-0.1.0/src/listmonk/public/resources.py +33 -0
  52. klarient_listmonk-0.1.0/src/listmonk/py.typed +1 -0
  53. klarient_listmonk-0.1.0/src/listmonk/subscribers/__init__.py +53 -0
  54. klarient_listmonk-0.1.0/src/listmonk/subscribers/requests.py +395 -0
  55. klarient_listmonk-0.1.0/src/listmonk/subscribers/resources.py +176 -0
  56. klarient_listmonk-0.1.0/src/listmonk/subscribers/responses.py +223 -0
  57. klarient_listmonk-0.1.0/src/listmonk/templates/__init__.py +35 -0
  58. klarient_listmonk-0.1.0/src/listmonk/templates/requests.py +82 -0
  59. klarient_listmonk-0.1.0/src/listmonk/templates/resources.py +64 -0
  60. klarient_listmonk-0.1.0/src/listmonk/templates/responses.py +92 -0
  61. klarient_listmonk-0.1.0/src/listmonk/transactional/__init__.py +14 -0
  62. klarient_listmonk-0.1.0/src/listmonk/transactional/requests.py +185 -0
  63. klarient_listmonk-0.1.0/src/listmonk/transactional/resources.py +11 -0
  64. klarient_listmonk-0.1.0/tests/test_listmonk_bounces.py +162 -0
  65. klarient_listmonk-0.1.0/tests/test_listmonk_campaigns.py +298 -0
  66. klarient_listmonk-0.1.0/tests/test_listmonk_imports.py +171 -0
  67. klarient_listmonk-0.1.0/tests/test_listmonk_lists.py +223 -0
  68. klarient_listmonk-0.1.0/tests/test_listmonk_media.py +122 -0
  69. klarient_listmonk-0.1.0/tests/test_listmonk_subscribers.py +348 -0
  70. klarient_listmonk-0.1.0/tests/test_listmonk_templates.py +172 -0
  71. klarient_listmonk-0.1.0/tests/test_listmonk_transactional.py +143 -0
@@ -0,0 +1,69 @@
1
+ name: Publish Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ name: Test
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - name: Check out source
18
+ uses: actions/checkout@v7
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v6
22
+ with:
23
+ python-version: "3.13"
24
+ cache: pip
25
+
26
+ - name: Install test dependencies
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ python -m pip install -e ".[dev]"
30
+
31
+ - name: Run tests
32
+ run: python -m pytest
33
+
34
+ publish:
35
+ name: Build and publish
36
+ runs-on: ubuntu-latest
37
+ needs: test
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/p/klarient-listmonk
41
+ permissions:
42
+ contents: read
43
+ id-token: write
44
+
45
+ steps:
46
+ - name: Check out source
47
+ uses: actions/checkout@v7
48
+ with:
49
+ fetch-depth: 0
50
+
51
+ - name: Set up Python
52
+ uses: actions/setup-python@v6
53
+ with:
54
+ python-version: "3.13"
55
+ cache: pip
56
+
57
+ - name: Install build tools
58
+ run: |
59
+ python -m pip install --upgrade pip
60
+ python -m pip install build twine
61
+
62
+ - name: Build package
63
+ run: python -m build
64
+
65
+ - name: Check package metadata
66
+ run: python -m twine check dist/*
67
+
68
+ - name: Publish package
69
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,158 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Password files
7
+ *.api_key
8
+ settings.json
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
109
+ __pypackages__/
110
+
111
+ # Celery stuff
112
+ celerybeat-schedule
113
+ celerybeat.pid
114
+
115
+ # SageMath parsed files
116
+ *.sage.py
117
+
118
+ # Environments
119
+ .env
120
+ .venv
121
+ env/
122
+ venv/
123
+ ENV/
124
+ env.bak/
125
+ venv.bak/
126
+
127
+ # Spyder project settings
128
+ .spyderproject
129
+ .spyproject
130
+
131
+ # Rope project settings
132
+ .ropeproject
133
+
134
+ # mkdocs documentation
135
+ /site
136
+
137
+ # mypy
138
+ .mypy_cache/
139
+ .dmypy.json
140
+ dmypy.json
141
+
142
+ # Pyre type checker
143
+ .pyre/
144
+
145
+ # pytype static type analyzer
146
+ .pytype/
147
+
148
+ # Cython debug symbols
149
+ cython_debug/
150
+
151
+ # PyCharm
152
+ # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
153
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
154
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
155
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
156
+ .idea/
157
+ *.db
158
+ .codiumai
@@ -0,0 +1,365 @@
1
+ Metadata-Version: 2.4
2
+ Name: klarient-listmonk
3
+ Version: 0.1.0
4
+ Summary: A typed Python client for the Listmonk API built with Klarient.
5
+ Author-email: Ludvik Jerabek <83429267+ludvikjerabek@users.noreply.github.com>
6
+ License-Expression: MIT
7
+ Project-URL: Documentation, https://github.com/ludvikjerabek/klarient-listmonk#readme
8
+ Project-URL: Issues, https://github.com/ludvikjerabek/klarient-listmonk/issues
9
+ Project-URL: Repository, https://github.com/ludvikjerabek/klarient-listmonk
10
+ Keywords: api,klarient,listmonk,rest,typed
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: klarient[requests]>=0.2.0
22
+ Provides-Extra: httpx
23
+ Requires-Dist: klarient[httpx]>=0.2.0; extra == "httpx"
24
+ Provides-Extra: dev
25
+ Requires-Dist: klarient[dev]>=0.2.0; extra == "dev"
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+
28
+ # Listmonk Client
29
+
30
+ This package is a typed Python wrapper for the [Listmonk](https://listmonk.app/)
31
+ REST API built with Klarient.
32
+
33
+ The wrapper models Listmonk's API as a resource tree. The Python object you use
34
+ matches the URI shape you are calling, so it is easy to move between Listmonk's
35
+ API docs and client code.
36
+
37
+ ```python
38
+ from listmonk import ListMonkClient
39
+
40
+ client = ListMonkClient(
41
+ base_url="https://listmonk.example.com",
42
+ username="api-user",
43
+ access_token="api-token",
44
+ )
45
+
46
+ print(client.lists.path) # /api/lists
47
+ print(client.lists[1].path) # /api/lists/1
48
+ print(client.templates[1].preview.path) # /api/templates/1/preview
49
+ ```
50
+
51
+ ## Installation
52
+
53
+ If this wrapper is published separately, install that package directly:
54
+
55
+ ```bash
56
+ python3 -m pip install klarient-listmonk
57
+ ```
58
+
59
+ If the final package name changes, install that package instead. For local
60
+ development from this folder, use an editable install:
61
+
62
+ ```bash
63
+ python3 -m pip install -e .
64
+ ```
65
+
66
+ The package would depend on Klarient and one supported sync HTTP backend. The
67
+ current wrapper is synchronous and defaults to the requests transport.
68
+
69
+ ## Quick Start
70
+
71
+ ```python
72
+ from listmonk import ListMonkClient
73
+ from listmonk.common import PerPage
74
+ from listmonk.lists.requests import ListQuery
75
+
76
+ client = ListMonkClient(
77
+ base_url="https://listmonk.example.com",
78
+ username="api-user",
79
+ access_token="api-token",
80
+ )
81
+
82
+ lists = client.lists.retrieve(ListQuery().with_per_page(PerPage.ALL))
83
+
84
+ for item in lists.data.results:
85
+ print(item.id, item.name)
86
+ ```
87
+
88
+ ## Using A Proxy
89
+
90
+ `ListMonkClient` defaults to Klarient's requests transport. Proxy settings can
91
+ be passed through `native_options`, which are forwarded to the underlying
92
+ requests call.
93
+
94
+ ```python
95
+ from listmonk import ListMonkClient
96
+
97
+ client = ListMonkClient(
98
+ base_url="https://listmonk.example.com",
99
+ username="api-user",
100
+ access_token="api-token",
101
+ native_options={
102
+ "proxies": {
103
+ "http": "http://proxy.example.com:8080",
104
+ "https": "http://proxy.example.com:8080",
105
+ },
106
+ },
107
+ )
108
+
109
+ lists = client.lists.retrieve()
110
+ ```
111
+
112
+ If your environment already uses standard proxy variables, requests can also
113
+ pick them up automatically:
114
+
115
+ ```bash
116
+ export HTTP_PROXY="http://proxy.example.com:8080"
117
+ export HTTPS_PROXY="http://proxy.example.com:8080"
118
+ ```
119
+
120
+ Use `native_options` when you want the proxy configuration to live with the
121
+ client instead of the process environment.
122
+
123
+ ## Resource Tree
124
+
125
+ The root client exposes the primary Listmonk API areas:
126
+
127
+ ```python
128
+ client.subscribers
129
+ client.lists
130
+ client.imports
131
+ client.campaigns
132
+ client.media
133
+ client.templates
134
+ client.tx
135
+ client.transactional
136
+ client.bounces
137
+ client.public
138
+ ```
139
+
140
+ Collection resources can expose item resources by indexing:
141
+
142
+ ```python
143
+ subscriber = client.subscribers[123].retrieve()
144
+ template_html = client.templates[1].preview.retrieve()
145
+ ```
146
+
147
+ Nested endpoints are modeled as nested resources:
148
+
149
+ ```python
150
+ client.subscribers[123].bounces.retrieve()
151
+ client.campaigns.running.stats.retrieve(...)
152
+ client.campaigns.analytics["views"].retrieve(...)
153
+ ```
154
+
155
+ ## Request Models
156
+
157
+ Request options are typed objects. You can pass values directly to constructors
158
+ for quick use or use chainable builders when a request has several optional
159
+ fields.
160
+
161
+ ```python
162
+ from listmonk.common import PerPage, SortOrder
163
+ from listmonk.subscribers.requests import SubscriberOrderBy, SubscriberQuery
164
+
165
+ query = (
166
+ SubscriberQuery()
167
+ .with_order_by(SubscriberOrderBy.NAME)
168
+ .with_order(SortOrder.ASC)
169
+ .with_per_page(PerPage.ALL)
170
+ )
171
+
172
+ response = client.subscribers.retrieve(query)
173
+ ```
174
+
175
+ Repeated query parameters, JSON bodies, form bodies, and multipart bodies are
176
+ handled by the request model for each endpoint.
177
+
178
+ ## Common Examples
179
+
180
+ Retrieve subscribers:
181
+
182
+ ```python
183
+ from listmonk.common import PerPage
184
+ from listmonk.subscribers.requests import SubscriberQuery
185
+
186
+ subscribers = client.subscribers.retrieve(
187
+ SubscriberQuery().with_per_page(PerPage.ALL)
188
+ )
189
+ ```
190
+
191
+ Preview an existing template:
192
+
193
+ ```python
194
+ preview = client.templates[1].preview.retrieve()
195
+ print(preview.data)
196
+ ```
197
+
198
+ Render a draft template body:
199
+
200
+ ```python
201
+ from listmonk.templates.requests import TemplatePreviewRender, TemplateType
202
+
203
+ html = """
204
+ <!doctype html>
205
+ <html>
206
+ <body>
207
+ {{ template "content" . }}
208
+ {{ TrackView }}
209
+ </body>
210
+ </html>
211
+ """.strip()
212
+
213
+ preview = client.templates.preview.render(
214
+ TemplatePreviewRender(
215
+ type=TemplateType.CAMPAIGN,
216
+ body=html,
217
+ )
218
+ )
219
+ ```
220
+
221
+ Listmonk expects `POST /api/templates/preview` to be form encoded, even though
222
+ template create and update use JSON. The wrapper hides that detail behind
223
+ `TemplatePreviewRender`.
224
+
225
+ Run a subscriber SQL filter:
226
+
227
+ ```python
228
+ from listmonk.common import PerPage
229
+ from listmonk.subscribers.requests import SubscriberSQLQuery
230
+
231
+ response = client.subscribers.sql_query.retrieve(
232
+ SubscriberSQLQuery()
233
+ .with_sql("subscribers.id > 0")
234
+ .with_per_page(PerPage.ALL)
235
+ )
236
+ ```
237
+
238
+ This calls `GET /api/subscribers` with Listmonk's SQL filter parameter. It
239
+ requires the `subscribers:sql_query` permission. Listmonk documents that
240
+ permission as powerful because it can bypass individual list and subscriber
241
+ permission boundaries, even though the query is read-only.
242
+
243
+ Upload media:
244
+
245
+ ```python
246
+ from listmonk.media.requests import MediaUpload
247
+
248
+ uploaded = client.media.upload(MediaUpload.from_file("logo.png"))
249
+ ```
250
+
251
+ Send a transactional message:
252
+
253
+ ```python
254
+ from listmonk.transactional.requests import (
255
+ TransactionalContentType,
256
+ TransactionalMessage,
257
+ TransactionalSubscriberMode,
258
+ )
259
+
260
+ message = (
261
+ TransactionalMessage()
262
+ .with_template(1)
263
+ .with_subscriber_mode(TransactionalSubscriberMode.EXTERNAL)
264
+ .add_subscriber_email("user@example.com")
265
+ .with_content_type(TransactionalContentType.HTML)
266
+ .with_data("name", "Jane")
267
+ )
268
+
269
+ client.tx.send(message)
270
+ ```
271
+
272
+ ## Endpoint Coverage
273
+
274
+ The wrapper models the documented Listmonk API areas:
275
+
276
+ - Bounces
277
+ - Campaigns
278
+ - Imports
279
+ - Lists
280
+ - Media
281
+ - Public list and subscription endpoints
282
+ - Subscribers
283
+ - Templates
284
+ - Transactional messages
285
+
286
+ It also exposes the subscriber SQL query capability as
287
+ `client.subscribers.sql_query`.
288
+
289
+ ## Usage Examples
290
+
291
+ The `usage/` directory contains endpoint-focused scripts that can be used as a
292
+ practical tour of the wrapper.
293
+
294
+ Copy the example settings file:
295
+
296
+ ```bash
297
+ cp usage/settings.example.json usage/settings.json
298
+ ```
299
+
300
+ Fill in your Listmonk URL and API credentials:
301
+
302
+ ```json
303
+ {
304
+ "base_url": "https://listmonk.example.com",
305
+ "username": "api-user",
306
+ "access_token": "api-token"
307
+ }
308
+ ```
309
+
310
+ Run one endpoint example at a time:
311
+
312
+ ```bash
313
+ python3 usage/lists.py
314
+ python3 usage/subscribers.py
315
+ python3 usage/campaigns.py
316
+ python3 usage/templates.py
317
+ ```
318
+
319
+ Most scripts are read-only by default. Actions that create, update, delete,
320
+ upload, blocklist, or send messages are guarded by settings flags such as:
321
+
322
+ ```json
323
+ {
324
+ "create_test_data": false,
325
+ "delete_import": false,
326
+ "delete_bounces": false,
327
+ "delete_uploaded_media": false
328
+ }
329
+ ```
330
+
331
+ Keep those disabled unless you are intentionally testing against disposable
332
+ data.
333
+
334
+ ## Project Layout
335
+
336
+ ```text
337
+ src/listmonk/
338
+ bounces/
339
+ campaigns/
340
+ imports/
341
+ lists/
342
+ media/
343
+ public/
344
+ subscribers/
345
+ templates/
346
+ transactional/
347
+ client.py
348
+ common.py
349
+ paging.py
350
+
351
+ usage/
352
+ bounces.py
353
+ campaigns.py
354
+ imports.py
355
+ lists.py
356
+ media.py
357
+ public.py
358
+ subscribers.py
359
+ templates.py
360
+ transactional.py
361
+ ```
362
+
363
+ Each API area groups its request models, response models, and resources together.
364
+ That keeps the wrapper close to the API documentation while still allowing
365
+ shared helpers for common types and pagination.