Files
MIND/docs/generate_api_docs.py
2025-08-21 15:52:09 +02:00

261 lines
7.9 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# autopep8: off
from os.path import dirname
from subprocess import run
from sys import path
from typing import List, Type, Union
path.insert(0, dirname(dirname(__file__)))
import frontend.api
from backend.base.custom_exceptions import (DatabaseFileNotFound,
NotificationServiceNotFound,
ReminderNotFound,
StaticReminderNotFound,
TemplateNotFound)
from backend.base.definitions import Constants, MindException, StartType
from backend.base.helpers import folder_path
from frontend.input_validation import API_DOCS, DataSource, InputVariable
# autopep8: on
API_PREFIX = Constants.API_PREFIX
ADMIN_PREFIX = Constants.ADMIN_PREFIX
API_FILE = folder_path('docs', 'src', 'other_docs', 'api.md')
url_var_map = {
'int:n_id': NotificationServiceNotFound,
'int:r_id': ReminderNotFound,
'int:t_id': TemplateNotFound,
'int:s_id': StaticReminderNotFound,
'int:b_idx': DatabaseFileNotFound
}
result = f"""# API
Below is the API documentation. Report an issue on [GitHub](https://github.com/Casvt/MIND/issues).
All endpoints have the `{API_PREFIX}` prefix. That means, for example, that `/auth/login` can be reached at `{API_PREFIX}/auth/login`.
## Authentication
Authentication is done using an API key.
To log in, make a POST request to the [`{API_PREFIX}/auth/login`](#authlogin) endpoint.
You'll receive an API key, which you can then use in your requests to authenticate.
Supply it via the url parameter `api_key`.
This API key is valid for one hour (though the admin can change this duration) after which the key expires, any further requests return 401 'APIKeyExpired' and you are required to log in again.
If no `api_key` is supplied or it is invalid, 401 `APIKeyInvalid` is returned.
For example:
```bash
curl -sSL 'http://192.168.2.15:8080{API_PREFIX}/reminders?api_key=ABCDEFG'
```
## Supplying data
Often, data needs to be supplied with a request:
- If the parameters need to be supplied via `url`, add them to the url as url parameters.
- If the parameters need to be supplied via `body`, add them to the body as a json object and supply the `Content-Type: application/json` header.
- If the parameters need to be supplied via `file`, send them as form data values and supply the `Content-Type: multipart/form-data` header.
For example:
```bash
# URL parameter
curl -sSL 'http://192.168.2.15:8080{API_PREFIX}/reminders/search?api_key=ABCDEFG&query=Fountain&sort_by=time_reversed'
# Body parameter
curl -sSLX POST \\
-H 'Content-Type: application/json' \\
-d '{{"title": "Test service", "url": "test://fake/url"}}' \\
'http://192.168.2.15:8080{API_PREFIX}/notificationservices?api_key=ABCDEFG'
# File parameter
curl -sSLX POST \\
-H 'Content-Type: multipart/form-data' \\
-F file=@/backups/MIND_backup.db \\
'http://192.168.2.15:8080{ADMIN_PREFIX}/database?api_key=ABCDEFG'
```
## Endpoints
The following is automatically generated. Please report any issues on [GitHub](https://github.com/Casvt/MIND/issues).
"""
def make_exception_instance(cls: Type[MindException]) -> MindException:
for args in (
(),
('1'),
('1', '2'),
('1', StartType.STARTUP)
):
try:
inst = cls(*args)
except (TypeError, AttributeError):
continue
else:
return inst
raise RuntimeError("Unsupported exception parameter")
def extract_url_var(endpoint: str) -> Union[str, None]:
split = endpoint.replace('<', '>').split('>')
return None if len(split) == 1 else split[1]
def rule_header(endpoint: str, requires_auth: bool, description: str) -> str:
return f"""### `{endpoint}`
| Requires being logged in | Description |
| ------------------------ | ----------- |
| {'Yes' if requires_auth else 'No'} | {description} |
"""
def url_var_note(endpoint: str, url_var: str) -> str:
return f"""
Replace `<{url_var}>` with the ID of the entry. For example: `{endpoint.replace(f'<{url_var}>', '2')}`.
"""
def method_body(name: str, description: str) -> str:
r = f"\n??? {name.upper()}\n"
if description:
r += f"\n {description}\n"
return r
def method_parameters(
var_type: str,
variables: List[Type[InputVariable]]
) -> str:
r = f"""
**Parameters ({var_type})**
| Name | Required | Data type | Description | Allowed values |
| ---- | -------- | --------- | ----------- | -------------- |
"""
for var in variables:
r += " | %s | %s | %s | %s | %s |\n" % (
var.name, 'Yes' if var.required else 'No',
','.join(v.value for v in var.data_type), var.description,
", ".join(str(v) for v in var.options) or "N/A"
)
return r
def return_codes(
method_name: str,
exceptions: List[MindException]
) -> str:
r = f"""
**Returns**
| Code | Error | Description |
| ---- | ----- | ----------- |
| {201 if method_name == 'post' else 200} | N/A | Success |
"""
for exception in exceptions:
r += " | %d | %s | %s |\n" % (
exception.api_response['code'], exception.api_response['error'],
exception.__doc__
)
return r
def create_result(base_string: str) -> str:
for endpoint, data in API_DOCS.items():
# Add header
base_string += rule_header(
endpoint,
data.requires_auth,
data.description
)
# Add note about url var
url_var = extract_url_var(endpoint)
if url_var:
base_string += url_var_note(endpoint, url_var)
# Add info for each method
for m_name, method in (
(m, data.methods[m])
for m in data.methods.used_methods()
):
if method is None:
continue
# Add basic method info
base_string += method_body(m_name, method.description)
# Add input variable info
var_types = {
'url': [
v for v in method.input_variables if v.source == DataSource.VALUES
],
'body': [
v for v in method.input_variables if v.source == DataSource.DATA
],
'file': [
v for v in method.input_variables if v.source == DataSource.FILES
]
}
for var_type, entries in var_types.items():
if entries:
base_string += method_parameters(var_type, entries)
url_exception = (
[url_var_map[url_var]]
if url_var in url_var_map else
[]
)
variable_exceptions = [
e
for v in method.input_variables
for e in v.related_exceptions
]
related_exceptions = sorted(
(
make_exception_instance(e)
for e in set(variable_exceptions + url_exception)
),
key=lambda e: (
e.api_response["code"],
e.api_response["error"]
)
)
base_string += return_codes(m_name, related_exceptions)
base_string += '\n'
return base_string.strip()
if __name__ == '__main__':
result = create_result(result)
with open(API_FILE, 'r') as f:
current_content = f.read()
if current_content == result:
print('Nothing changed')
else:
with open(API_FILE, 'w+') as f:
f.write(result)
run(["git", "config", "--global", "user.email", '"casvantijn@gmail.com"'])
run(["git", "config", "--global", "user.name", '"CasVT"'])
run(["git", "checkout", "Development"])
run(["git", "add", API_FILE])
run(["git", "commit", "-m", "Updated API docs"])
run(["git", "push"])