[docs] Zensical docs (#13253)

* add zensical workflow

* rebuild schema dynamically

* load in new module

* Use zensical for docs invoke tasks

* modern

* improved carousel layout

* Increased content width

* fix for app icons

* Ensure schema dir exists

* remove unused requirement

* update readthedocs process

* fix polar.sh link

* adjust link checker pattern

* Revert some of the original style

* remove legacy mkdocs requirements

* fix for ready.py

* reduce db warnings during schema generation

* Adjust padding

* fix index issues

* fix page title

* fix pages with broken or missing headers

* split data import API schema

* cleanup old carousel

* remove djlint link
This commit is contained in:
Oliver authored and GitHub committed 2026-11-14 22:34:18 +11:00
1 parent 80ad822688
commit 19dfb81bfb
52 files changed
+1272 -4885

No files matched your search

+1 -1
View File
@@ -53,7 +53,7 @@
"label": "InvenTree Server"
},
"8080": {
"label": "properdocs server"
"label": "Documentation server"
}
},
+1 -1
View File
@@ -129,7 +129,7 @@ jobs:
pip install --require-hashes --only-binary :all: -r docs/requirements.txt
- name: Build documentation
run: |
invoke build-docs --mkdocs
invoke build-docs --build
- name: Zip build docs
run: |
cd docs/site
+1
View File
@@ -12,6 +12,7 @@ site/
# Generated API schema files
docs/api/schema/*.yml
docs/api/schema/*.md
# Temporary cache files
url_cache.txt
+15 -9
View File
@@ -2,11 +2,11 @@
[![Documentation Status](https://readthedocs.org/projects/inventree/badge/?version=latest)](https://inventree.readthedocs.io/en/latest/?badge=latest)
This repository hosts the [official documentation](https://inventree.readthedocs.io/) for [InvenTree](https://github.com/inventree/inventree), an open source inventory management system.
This repository hosts the [official documentation](https://docs.inventree.org/) for [InvenTree](https://github.com/inventree/inventree), an open source inventory management system.
## Prerequisites
InvenTree uses [MkDocs](https://www.mkdocs.org/) to convert [Markdown](https://www.mkdocs.org/user-guide/writing-your-docs/#writing-with-markdown) format `.md` files into HTML suitable for viewing in a web browser.
InvenTree uses [Zensical](https://zensical.org/) to convert [Markdown](https://zensical.org/docs/authoring/markdown/) format `.md` files into HTML suitable for viewing in a web browser. The site configuration is in `docs/mkdocs.yml` (Zensical reads the mkdocs configuration format).
!!! info "Prerequisites"
To build and serve this documentation locally (e.g. for development), you will need:
@@ -31,7 +31,7 @@ $ source env/bin/activate
!!! info "(env) prefix"
The shell prompt should now display the `(env)` prefix, showing that you are operating within the context of the python virtual environment
You can now install the additional packages needed by mkdocs:
You can now install the additional packages needed to build the documentation:
```
$ cd src
@@ -40,7 +40,7 @@ $ pip install --require-hashes -r docs/requirements.txt
## Schema generation
Building the documentation requires extracting the API schema from the source code.
Building the documentation requires extracting the API schema (and settings definitions) from the source code. The API reference pages in `docs/docs/api/schema/` are generated from this schema and are not tracked in git.
!!! tip
This command is only required when building the documentation for the first time, or when changes have been made to the API schema.
@@ -62,15 +62,21 @@ Exporting definitions complete
Exporting schema file to '/home/inventree/src/docs/generated/schema.yml'
Schema export completed: /home/inventree/src/docs/generated/schema.yml
Documentation build complete, but properdocs not requested
Documentation build complete, but zensical build not requested
```
To generate the schema files *and* build the static HTML site (in `docs/site/`), including the post-build checks that all settings are documented, run:
```
$ invoke build-docs --build
```
## Viewing the documentation
Generate the HTML files from the markdown source files, and start the properdocs webpage server:
Generate the HTML files from the markdown source files, and start the zensical webpage server:
```
$ properdocs serve -f docs/mkdocs.yml
$ zensical serve -a localhost:8080 -f docs/mkdocs.yml
```
You can then point your web browser at http://localhost:8080/
@@ -81,7 +87,7 @@ Alternatively, you can use the `invoke` command:
$ invoke dev.docs-server
```
If you need to, use the `-a` option after `properdocs` or `invoke` to set the address and port. Run `invoke dev.docs-server --help` for details.
If you need to, use the `-a` option after `zensical serve` or `invoke` to set the address and port. Run `invoke dev.docs-server --help` for details.
## Editing the Documentation Files
@@ -146,4 +152,4 @@ Global variables should be added in the `# Global Variables` section of the `mkd
## Credits
This documentation makes use of the [mkdocs-material template](https://github.com/squidfunk/mkdocs-material)
This documentation is built with [Zensical](https://zensical.org/), from the team behind [Material for MkDocs](https://github.com/squidfunk/mkdocs-material)
+28 -10
View File
@@ -11,22 +11,40 @@
</div>
<script>
document.addEventListener( 'DOMContentLoaded', function () {
new Splide(
// With navigation.instant, page content is swapped in without a new
// DOMContentLoaded event, so mount immediately if the document has loaded
(function () {
function mountCarousel() {
if (window.carouselInstance) {
window.carouselInstance.destroy();
}
window.carouselInstance = new Splide(
'.splide', {
type: 'loop',
perPage: {{ per_page }},
autoplay: true,
pagination: false,
// cover: true,
// autoWidth: true,
height: '25rem',
// Each slide is as wide as its image, so that all images
// are scaled to the same height (set in extra.css)
autoWidth: true,
height: '{{ height | default("14rem") }}',
breakpoints: {
// Narrow screens (e.g. mobile)
700: {
height: '{{ mobile_height | default("8rem") }}',
},
},
gap: '1rem',
focus: 'center',
padding: {
left: '5rem',
right: '5rem',
}
}
).mount();
} );
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountCarousel);
} else {
mountCarousel();
}
})();
</script>
+4
View File
@@ -20,3 +20,7 @@ To export API data to a file, add the `&export=<format>` modifier to the query.
Any other query filters used in the API request are also observed when downloading the data. For example, to download a list of all stock items in a given location:
`<host>/api/stock/?format=csv&location=10`
### Further Information
For further information, refer to the [data export documentation](../concepts/data_export.md).
+3 -1
View File
@@ -2,12 +2,14 @@
title: InvenTree API Schema
---
## InvenTree API Schema
The InvenTree API is implemented using the [Django REST framework](https://www.django-rest-framework.org).
The API schema as documented below is generated using the [drf-spectactular](https://github.com/tfranzel/drf-spectacular/) extension.
## API Version
This documentation is for API version: `568`
This documentation is for API version: `578`
!!! tip "API Schema History"
We track API schema changes, and provide a snapshot of each API schema version in the [API schema repository](https://github.com/inventree/schema/).
-7
View File
@@ -1,7 +0,0 @@
---
title: Authorization and Authentication
---
The *Authorization and Authentication* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/auth.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Background Task Management
---
The *Background Task Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/background-task.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Barcode Scanning
---
The *Barcode Scanning* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/barcode.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Bill of Materials
---
The *Bill of Materials* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/bom.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Build Order Management
---
The *Build Order Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/build.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Company Management
---
The *Company Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/company.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: General API Endpoints
---
The *General API Endpoints* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/general.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Label Printing
---
The *Label Printing* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/label.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: External Machine Management
---
The *External Machine Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/machine.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: External Order Management
---
The *External Order Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/order.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Parts and Part Categories
---
The *Parts and Part Categories* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/part.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Plugin Functionality
---
The *Plugin Functionality* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/plugins.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Part Pricing
---
The *Part Pricing* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/pricing.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Report Generation
---
The *Report Generation* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/report.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Settings Management
---
The *Settings Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/settings.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: Stock and Stock Locations
---
The *Stock and Stock Locations* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/stock.yml)]
-7
View File
@@ -1,7 +0,0 @@
---
title: User Management
---
The *User Management* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/user.yml)]
+8 -4
View File
@@ -2,14 +2,16 @@
title: InvenTree Mobile App
---
{% with directory="appgallery", per_page=2 %}
## InvenTree Mobile App
The InvenTree Mobile App brings stock control to your pocket. Integrating seamlessly with the [InvenTree API](../api/index.md), the app provides immediate access to inventory data without requiring physical access to a computer.
{% with directory="appgallery", per_page=2, height="25rem", mobile_height="25rem" %}
{% include "carousel.html" %}
{% endwith %}
-----
The InvenTree Mobile App brings stock control to your pocket. Integrating seamlessly with the [InvenTree API](../api/index.md), the app provides immediate access to inventory data without requiring physical access to a computer.
Native barcode support provides a multitude of context-sensitive stock control actions, allowing streamlined inventory management at your fingertips. The app has been optimized for speed, providing instant access to stock knowledge and handy on-site functionality.
## Features
@@ -29,7 +31,9 @@ The InvenTree app can be downloaded from either the Android or Apple app stores,
<span class='fab fa-android'></span> [Android Play Store](https://play.google.com/store/apps/details?id=inventree.inventree_app).
!!! info "APK Download"
As an alternative to using the Android Play Store, the InvenTree app APK file can be downloaded directly via our [polar.sh](https://polar.sh/inventree/products/299bf0d5-af88-4e0f-becf-c007ad37ecf2) page.
As an alternative to using the Android Play Store, the InvenTree app APK file can be downloaded directly via our [polar.sh](https://buy.polar.sh/polar_cl_UnGILJ0c7P3hQrOrJs127oyLTTDOTHKrnqfCg30XtBI) page.
### iOS
-1
View File
@@ -2,7 +2,6 @@
title: Companies
---
## Companies
External companies are represented by the *Company* database model. Each company may be classified into the following categories:
+3 -3
View File
@@ -3,9 +3,9 @@ title: Contribution Guide
---
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
## Contribution Guide
## Quickstart
Please read the contribution guidelines below, before submitting your first pull request to the InvenTree codebase.
The following commands will get you quickly configure and run a development server, complete with a demo dataset to work with:
@@ -281,7 +281,7 @@ If you have followed the setup devtools procedure, then code style checking is p
Django are checked by [djlint](https://github.com/Riverside-Healthcare/djlint) through pre-commit.
The following rules out of the [default set](https://djlint.com/docs/linter/) are not applied:
The following rules out of the default set of djlint rules are not applied:
```bash
D018: (Django) Internal links should use the { % url ... % } pattern
H006: Img tag should have height and width attributes
+1 -1
View File
@@ -2,7 +2,7 @@
title: React Frontend Development
---
## Setup
## React Frontend Development
The following documentation details how to setup and run a development installation of the InvenTree frontend user interface. It is assumed that you are using the [InvenTree devcontainer setup](./devcontainer.md).
-14
View File
@@ -6,16 +6,6 @@ title: High level roadmap
## High level Epics
### 1.0
Smaller items can be viewed [on the milestone](https://github.com/inventree/InvenTree/issues?q=is%3Aissue%20milestone%3A1.0.0).
Aiming to stabelise several aspects of the software:
- Only shipping the new frontend and removing reliance on templating
- Stabelize API for client generation
- Making data import- / export-mechanisms more stable
- Updating and Re-Organising documentation to enable CII best practices compliance
### 2.0
@@ -29,7 +19,3 @@ Smaller items can be viewed [on the milestone](https://github.com/inventree/Inve
### Future
There are several epics that target [the horizion](https://github.com/inventree/InvenTree/issues?q=is%3Aissue%20state%3Aopen%20type%3AEpic).
## Non-Goals
TBD
+6 -1
View File
@@ -1,4 +1,9 @@
"""Custom mkdocs hooks, using the mkdocs-simple-hooks plugin."""
"""Documentation build hooks.
Zensical does not run mkdocs hooks, so these are called directly:
- on_config: via docs/docs_data.py (hook_config)
- on_post_build: via the "invoke build-docs --build" task
"""
import json
import os
File diff suppressed because it is too large. Load diff
+2
View File
@@ -1,4 +1,6 @@
## InvenTree Project Governance
As a MIT licensed open-source project, there are not legal obligations for the InvenTree project team or any contributor to provide support or maintenance. The software is provided "as-is" - without warranty of any kind.
However, the project team is committed to providing a reliable and [secure](../security.md) code base with predictable upgrade paths.
+2
View File
@@ -2,6 +2,8 @@
title: Sponsored Resources
---
## Sponsored Resources
The InvenTree project is grateful to receive resources from various vendors free of charge.
Individuals and companies can also support via [GitHub sponsors](https://github.com/sponsors/inventree).
+2
View File
@@ -2,6 +2,8 @@
title: Helper Functions
---
## Report Helper Functions
Some common functions are provided for use in custom report and label templates. To include these, load the `report` functions at the start of the template:
```html
-1
View File
@@ -1,4 +1,3 @@
---
title: Sales Orders
---
+1
View File
@@ -1,3 +1,4 @@
## InvenTree Security Overview
The InvenTree project is committed to providing a secure and safe environment for all users. We know that many of our users rely on InvenTree to manage the inventory and manufacturing for their small and mid-size businesses, and we take that responsibility seriously.
-16
View File
@@ -1,16 +0,0 @@
---
title: Experimental Features
---
## Feature Flags
InvenTree ships with django-flags and enables path (parameter), user, session, date or settings based feature flags. This allows admins to slowly test and roll out new features on their instance without running parallel instances.
Additional flags can be provided via the the `INVENTREE_FLAGS` environment key (see [configuration](../start/config.md#environment-variables)).
Superusers can configure run-time conditions [as per django-flags](https://cfpb.github.io/django-flags/conditions/) docs under `/admin/flags/flagstate/`.
## Current Experimental Features
| Feature | Key | Description |
| --- | --- | --- |
-1
View File
@@ -1,4 +1,3 @@
---
title: Transfer Orders
---
+92 -3
View File
@@ -68,12 +68,20 @@ p {
text-align: justify;
}
div:nth-of-type(1) p {
div:nth-of-type(1) p:not(.admonition-title) {
padding-top: 12px;
}
/* Remove page title */
.md-content h1:first-child {
/* Use the theme code font (bootstrap.css otherwise overrides it) */
.md-typeset code,
.md-typeset kbd,
.md-typeset pre {
font-family: var(--md-code-font-family);
}
/* Remove page title (Zensical inserts a skip-link anchor before the h1) */
.md-content h1:first-child,
.md-content #__skip + h1 {
display: none;
}
@@ -154,3 +162,84 @@ div:nth-of-type(1) p {
background-color: #5bc0de;
color: #555;
}
/* HTTP method badges for the generated API schema documentation */
.api-method {
display: inline-block;
min-width: 4.2em;
padding: 0.1em 0.5em;
border-radius: 0.2rem;
color: #fff;
font-family: var(--md-code-font-family);
font-size: 0.75em;
font-weight: 700;
text-align: center;
vertical-align: middle;
}
.api-method-get { background-color: #2b8a3e; }
.api-method-post { background-color: #1971c2; }
.api-method-put { background-color: #e67700; }
.api-method-patch { background-color: #9c36b5; }
.api-method-delete { background-color: #c92a2a; }
.api-method-head, .api-method-options { background-color: #495057; }
/* Image carousel: scale every image to the slide height, keeping aspect ratio */
.splide__slide {
display: flex;
align-items: center;
justify-content: center;
}
.splide__slide img {
height: 100%;
width: auto;
/* Very wide images shrink to fit the carousel on narrow screens */
max-width: calc(100vw - 2rem);
object-fit: contain;
}
/* Widen the page layout (theme default is 61rem), and keep ~5% horizontal
* padding so the content does not run to the window edges */
.md-grid {
max-width: 80rem;
padding-left: 5%;
padding-right: 5%;
}
/* ...but not for grids nested inside page content (e.g. the home page hero) */
.md-content .md-grid {
padding-left: 0;
padding-right: 0;
}
/* Solid primary-colour header and navigation tabs, as in the classic theme.
* (The Zensical "modern" variant uses a translucent neutral header.) */
.md-header,
.md-tabs {
background-color: var(--md-primary-fg-color);
color: var(--md-primary-bg-color);
}
.md-header {
backdrop-filter: none;
}
/* Search button on the coloured header (darker blue box, white text) */
.md-header .md-search__button {
background-color: rgba(0, 0, 0, 0.26);
color: var(--md-primary-bg-color);
}
.md-header .md-search__button:hover {
background-color: rgba(0, 0, 0, 0.32);
}
.md-header .md-search__button::before {
background-color: var(--md-primary-bg-color);
}
.md-header .md-search__button::after {
background-color: rgba(0, 0, 0, 0.2);
color: var(--md-primary-bg-color);
}
@@ -0,0 +1,8 @@
/*!
* Font Awesome Free 5.13.0 - SVG with JS stylesheet (https://fontawesome.com)
* License - https://fontawesome.com/license/free (Code: MIT License)
*
* fontawesome.js normally injects this into <head> at runtime, but with
* navigation.instant the head is replaced on page change and it is lost.
*/
svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;overflow:visible;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top left;transform-origin:top left}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0);transform:rotate(0)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-webkit-transform:scale(1,-1);transform:scale(1,-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1,-1);transform:scale(-1,-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-rotate-90{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-Line truncated
File diff suppressed because it is too large. Load diff
+194
View File
@@ -0,0 +1,194 @@
"""Shared data for the documentation build process.
This module loads the generated InvenTree definition files (settings, status codes,
report context, etc) which are used by the custom macros defined in 'main.py'.
It is deliberately separate from 'main.py', as some static site generators
(e.g. zensical) re-execute the macros module for *every* page. As a regular
module, this file is imported (and cached) only once per build process:
- Generated data files are loaded lazily, and reloaded only if they change on disk
- Observed settings / status codes are accumulated across all pages of the build
- The 'on_config' hook values are computed once (for generators without hook support)
"""
import importlib.util
import json
import os
import threading
from pathlib import Path
from typing import Any, Optional
import yaml
HERE = Path(__file__).parent
GENERATED_DIR = HERE.joinpath('generated')
# Files where we *store* information on what has been documented during the build.
# These are checked after the build completes (see 'on_post_build' in docs/hooks.py)
OBSERVED_SETTINGS_FILE = GENERATED_DIR.joinpath('observed_settings.json')
OBSERVED_STATUS_CODES_FILE = GENERATED_DIR.joinpath('observed_status_codes.json')
_lock = threading.RLock()
# Cache of loaded data files: {filename: (mtime, data)}
_file_cache: dict[str, tuple[float, Any]] = {}
# Things which have been rendered somewhere in the documentation
_observed_settings: dict[str, dict[str, bool]] = {'global': {}, 'user': {}}
_observed_status_codes: dict[str, bool] = {}
# Cached result of the 'on_config' hook
_hook_config: Optional[dict] = None
def print_build_environment():
"""Print some useful debugging information - useful for diagnosing CI build issues.
Ref: https://docs.readthedocs.io/en/stable/reference/environment-variables.html
"""
print('loading ./docs/docs_data.py...')
for key in [
'GITHUB_ACTIONS',
'GITHUB_REF',
'READTHEDOCS',
'READTHEDOCS_GIT_IDENTIFIER',
'READTHEDOCS_GIT_CLONE_URL',
'READTHEDOCS_GIT_COMMIT_HASH',
'READTHEDOCS_PROJECT',
'READTHEDOCS_VERSION',
'READTHEDOCS_VERSION_NAME',
'READTHEDOCS_VERSION_TYPE',
]:
val = os.environ.get(key, None) or '-- MISSING --'
print(f' - {key}: {val}')
def _write_json(path: Path, data: Any):
"""Write data to a JSON file."""
with open(path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
def reset_observations():
"""Reset the record of observed settings and status codes.
Called once when this module is first imported (i.e. once per build process).
"""
with _lock:
_observed_settings.clear()
_observed_settings.update({'global': {}, 'user': {}})
_observed_status_codes.clear()
_write_json(OBSERVED_SETTINGS_FILE, _observed_settings)
_write_json(OBSERVED_STATUS_CODES_FILE, _observed_status_codes)
def observe_setting(key: str, group: str):
"""Record that a particular setting has been documented.
Arguments:
key: The name of the setting
group: The group of the setting (e.g. 'global', 'user', 'config')
"""
with _lock:
group_data = _observed_settings.setdefault(group, {})
if key not in group_data:
group_data[key] = True
_write_json(OBSERVED_SETTINGS_FILE, _observed_settings)
def observe_status_code(class_name: str):
"""Record that a particular status code class has been documented."""
with _lock:
if class_name not in _observed_status_codes:
_observed_status_codes[class_name] = True
_write_json(OBSERVED_STATUS_CODES_FILE, _observed_status_codes)
def load_file(filename: str) -> Any:
"""Load a generated data file (JSON or YAML), caching the result.
The file is reloaded only if it has been modified since it was last loaded.
Arguments:
filename: Name of the file, relative to the 'generated' directory
"""
path = GENERATED_DIR.joinpath(filename)
mtime = path.stat().st_mtime
with _lock:
cached = _file_cache.get(filename)
if cached and cached[0] == mtime:
return cached[1]
with open(path, encoding='utf-8') as f:
if path.suffix in ('.yml', '.yaml'):
data = yaml.load(f, yaml.BaseLoader)
else:
data = json.load(f)
_file_cache[filename] = (mtime, data)
return data
def settings(group: str) -> dict:
"""Return the settings definitions for a given group ('global', 'user', 'config')."""
return load_file('inventree_settings.json')[group]
def tags() -> Any:
"""Return the custom template tags definitions."""
return load_file('inventree_tags.yml')
def filters() -> Any:
"""Return the custom template filters definitions."""
return load_file('inventree_filters.yml')
def status_codes() -> dict:
"""Return the status code class definitions."""
return load_file('inventree_status_codes.json')
def report_context() -> dict:
"""Return the report context definitions."""
return load_file('inventree_report_context.json')
def roles() -> Any:
"""Return the user permission role definitions."""
return load_file('inventree_roles.json')
def audited_models() -> Any:
"""Return the audited model definitions."""
return load_file('inventree_audited_models.json')
def hook_config(site_url: Optional[str] = None) -> dict:
"""Return the config values provided by the 'on_config' hook in docs/hooks.py.
Zensical does not run mkdocs hooks, so
the hook is run once here, and the result cached for subsequent pages.
"""
global _hook_config
with _lock:
if _hook_config is None:
spec = importlib.util.spec_from_file_location(
'inventree_docs_hooks', HERE.joinpath('docs', 'hooks.py')
)
hooks = importlib.util.module_from_spec(spec)
spec.loader.exec_module(hooks)
_hook_config = hooks.on_config({'site_url': site_url})
return _hook_config
print_build_environment()
reset_observations()
+15 -94
View File
@@ -2,10 +2,10 @@
import argparse
import os
import re
import textwrap
import yaml
from render_schema import SchemaRenderer
OUTPUT_DIR = './docs/api/schema/'
@@ -20,12 +20,13 @@ SPECIAL_PATHS = {
'bom': 'Bill of Materials',
'build': 'Build Order Management',
'company': 'Company Management',
'importer': 'Data Importing',
'label': 'Label Printing',
'machine': 'External Machine Management',
'order': 'External Order Management',
'part': 'Parts and Part Categories',
'plugins': 'Plugin Functionality',
'pricing': 'Part Pricing',
'pricing': 'Part and Stock Pricing',
'report': 'Report Generation',
'settings': 'Settings Management',
'stock': 'Stock and Stock Locations',
@@ -54,27 +55,20 @@ def top_level_path(path: str) -> str:
return GENERAL_PATH
def generate_schema_file(key: str) -> None:
"""Generate a schema file for the provided key."""
def generate_schema_file(key: str, renderer: SchemaRenderer, paths: dict) -> None:
"""Render the API documentation page for the provided key."""
description = SPECIAL_PATHS.get(key, 'General API Endpoints')
output = f"""
---
title: {description}
---
The *{description}* section of the InvenTree API schema is documented below.
[OAD(./docs/docs/api/schema/{key}.yml)]
"""
output = textwrap.dedent(output).strip() + '\n'
output = renderer.render_page(description, paths)
output_file = os.path.join(os.path.dirname(__file__), OUTPUT_DIR, f'{key}.md')
output_file = os.path.abspath(output_file)
print('Writing schema file to:', output_file)
# Generated pages are not tracked in git, so the directory may not exist yet
os.makedirs(os.path.dirname(output_file), exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(output)
@@ -125,99 +119,26 @@ def generate_index_file(version: str):
f.write(output)
def extract_refs(data: dict, components: dict) -> dict:
"""Extract a dict of refs from the provided paths dict.
The refs are located like so:
<path>:<method>:responses:<status>:content:application/json:schema:$ref
Also, the referenced components might reference *sub* components,
so we need to do this step recursively.
"""
pattern = r"'?\$ref'?: '#\/components\/schemas\/(\S+)'"
# First, extract the results from the paths data dict
matches = re.findall(pattern, str(data))
refs = list(matches)
# Next, extract additional refs from the components dict
# Note that the components dict may reference other components
idx = 0
while idx < len(refs):
ref = refs[idx]
schema = components.get('schemas', {}).get(ref, None)
if schema:
# Search for additional refs
matches = re.findall(pattern, str(schema))
for match in matches:
if match not in refs:
refs.append(match)
idx += 1
# Return a dict only of the extracted refs
schemas = {ref: components['schemas'][ref] for ref in refs}
return schemas
def parse_api_file(filename: str):
"""Parse the input API file, and split into smaller sections.
"""Parse the input API file, and render it as a set of smaller documentation pages.
The intent is to make the API schema easier to peruse on the documentation.
"""
with open(filename, encoding='utf-8') as f:
data = yaml.safe_load(f)
paths = data['paths']
top_level_paths = {}
for path, methods in paths.items():
for path, methods in data['paths'].items():
tlp = top_level_path(path)
top_level_paths.setdefault(tlp, {})[path] = methods
if tlp not in top_level_paths:
top_level_paths[tlp] = {}
top_level_paths[tlp][path] = methods
# The renderer uses the complete schema, so all references can be resolved
renderer = SchemaRenderer(data)
# Generate output files
for key, value in top_level_paths.items():
output_file = os.path.join(os.path.dirname(__file__), OUTPUT_DIR, f'{key}.yml')
output = {}
output['paths'] = value
components = data.get('components', {})
# Extract only the schemas relating to the provided paths
path_schemas = extract_refs(value, components)
for k, v in data.items():
if k == 'components':
v = v.copy()
v['schemas'] = path_schemas
if k != 'paths':
output[k] = v
print(f'Writing schema file to {output_file}...')
output_file = os.path.abspath(output_file)
with open(output_file, 'w', encoding='utf-8') as f:
yaml.dump(output, f)
# Generate a markdown file for the schema
generate_schema_file(key)
generate_schema_file(key, renderer, value)
# Finally, generate an index file for the API schema
generate_index_file(data['info']['version'])
+40 -127
View File
@@ -1,9 +1,8 @@
"""Main entry point for the documentation build process."""
import json
import logging
import os
import subprocess
import sys
import textwrap
from pathlib import Path
from typing import Literal, Optional
@@ -11,89 +10,17 @@ from typing import Literal, Optional
import requests
import yaml
# Debugging output - useful for diagnosing CI build issues
print('loading ./docs/main.py...')
logging.getLogger('openapidocs').setLevel(logging.ERROR)
# Print out some useful debugging information
# Ref: https://docs.readthedocs.io/en/stable/reference/environment-variables.html
for key in [
'GITHUB_ACTIONS',
'GITHUB_REF',
'READTHEDOCS',
'READTHEDOCS_GIT_IDENTIFIER',
'READTHEDOCS_GIT_CLONE_URL',
'READTHEDOCS_GIT_COMMIT_HASH',
'READTHEDOCS_PROJECT',
'READTHEDOCS_VERSION',
'READTHEDOCS_VERSION_NAME',
'READTHEDOCS_VERSION_TYPE',
]:
val = os.environ.get(key, None) or '-- MISSING --'
print(f' - {key}: {val}')
# Cached settings dict values
global CONFIG_SETTINGS
global GLOBAL_SETTINGS
global USER_SETTINGS
global TAGS
global FILTERS
global REPORT_CONTEXT
global STATUS_CODES
global ROLES
# Read in the InvenTree settings file
here = Path(__file__).parent
gen_base = here.joinpath('generated')
# File where we expect to find the settings definitions
settings_file = gen_base.joinpath('inventree_settings.json')
# Generated data is loaded (and cached) by a separate module, as some static site
# generators (e.g. zensical) re-execute this file for every page.
if str(here) not in sys.path:
sys.path.append(str(here))
# File where we will *store* information on the settings we have observed
observed_settings_file = gen_base.joinpath('observed_settings.json')
# Overwrite the observed settings file
with open(observed_settings_file, 'w', encoding='utf-8') as f:
data = {'global': {}, 'user': {}}
# Write an empty dict to the file
# This is used to track which settings we have observed during the build process
f.write(json.dumps(data, indent=4))
# File where we will *store* information on the status code classes we have observed
observed_status_codes_file = gen_base.joinpath('observed_status_codes.json')
# Overwrite the observed status codes file
with open(observed_status_codes_file, 'w', encoding='utf-8') as f:
f.write(json.dumps({}, indent=4))
with open(settings_file, encoding='utf-8') as sf:
settings = json.load(sf)
GLOBAL_SETTINGS = settings['global']
USER_SETTINGS = settings['user']
CONFIG_SETTINGS = settings['config']
# Tags
with open(gen_base.joinpath('inventree_tags.yml'), encoding='utf-8') as f:
TAGS = yaml.load(f, yaml.BaseLoader)
# Filters
with open(gen_base.joinpath('inventree_filters.yml'), encoding='utf-8') as f:
FILTERS = yaml.load(f, yaml.BaseLoader)
# Status codes
with open(gen_base.joinpath('inventree_status_codes.json'), encoding='utf-8') as f:
STATUS_CODES = json.load(f)
# Report context
with open(gen_base.joinpath('inventree_report_context.json'), encoding='utf-8') as f:
REPORT_CONTEXT = json.load(f)
# User permission roles
with open(gen_base.joinpath('inventree_roles.json'), encoding='utf-8') as f:
ROLES = json.load(f)
# Audited models
with open(gen_base.joinpath('inventree_audited_models.json'), encoding='utf-8') as f:
AUDITED_MODELS = json.load(f)
import docs_data
def get_repo_url(raw=False):
@@ -161,9 +88,30 @@ def get_build_environment() -> Optional[str]:
return 'master'
def apply_hook_config(env):
"""Provide values normally injected by the 'on_config' mkdocs hook.
Zensical does not support mkdocs hooks, so 'config.releases',
'config.assets_dir' etc are never set. Run the hook ourselves and
inject the results into the template 'config' variable.
"""
template_config = env.variables.get('config')
if template_config is None:
return
hook_config = docs_data.hook_config(template_config.get('site_url'))
for key in ['releases', 'assets_dir', 'readthedocs', 'version_banner']:
if key in hook_config:
template_config[key] = hook_config[key]
def define_env(env):
"""Define custom environment variables for the documentation build process."""
config = env.config
apply_hook_config(env)
config = env.variables.get('config') or env.config
assets_dir = config.get('assets_dir', None)
if assets_dir is None:
@@ -326,18 +274,10 @@ def define_env(env):
(produced by the `export_status_codes` management command), so it can never
drift out of sync with the status codes actually defined in the source code.
"""
global STATUS_CODES
status_class = STATUS_CODES[class_name]
status_class = docs_data.status_codes()[class_name]
# Record that this status code class has been rendered somewhere in the docs
with open(observed_status_codes_file, encoding='utf-8') as f:
data = json.load(f)
data[class_name] = True
with open(observed_status_codes_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
docs_data.observe_status_code(class_name)
ret_data = '| Status | Value | Description |\n| --- | --- | --- |\n'
@@ -356,18 +296,7 @@ def define_env(env):
key: The name of the setting to observe
group: The group of the setting (e.g. 'global', 'user')
"""
# Read the observed settings file
with open(observed_settings_file, encoding='utf-8') as f:
data = json.load(f)
if group not in data:
data[group] = {}
data[group][key] = True
# Write the updated data back to the file
with open(observed_settings_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4)
docs_data.observe_setting(key, group)
@env.macro
def rendersetting(
@@ -403,8 +332,7 @@ def define_env(env):
short: If True, return a short version of the setting (default: False)
default: An optional default value to override the setting's default display value.
"""
global GLOBAL_SETTINGS
setting = GLOBAL_SETTINGS[key]
setting = docs_data.settings('global')[key]
# Settings are only 'observed' if they are displayed in full
if not short:
@@ -420,8 +348,7 @@ def define_env(env):
key: The name of the user setting to extract information for.
short: If True, return a short version of the setting (default: False)
"""
global USER_SETTINGS
setting = USER_SETTINGS[key]
setting = docs_data.settings('user')[key]
# Settings are only 'observed' if they are displayed in full
if not short:
@@ -442,8 +369,7 @@ def define_env(env):
key: The name of the configuration setting to extract information for.
default: An optional default value to override the setting's default display value.
"""
global CONFIG_SETTINGS
setting = CONFIG_SETTINGS[key]
setting = docs_data.settings('config')[key]
observe_setting(key, 'config')
@@ -460,11 +386,8 @@ def define_env(env):
@env.macro
def tags_and_filters():
"""Return a list of all tags and filters."""
global TAGS
global FILTERS
ret_data = ''
for ref in [['Tags', TAGS], ['Filters', FILTERS]]:
for ref in [['Tags', docs_data.tags()], ['Filters', docs_data.filters()]]:
ret_data += f'### {ref[0]}\n\n| Namespace | Name | Description |\n| --- | --- | --- |\n'
for value in ref[1]:
title = (
@@ -488,11 +411,9 @@ def define_env(env):
`users.ruleset.RULESET_CHOICES`), so it can never drift out of sync with
the roles actually defined in the source code.
"""
global ROLES
ret_data = '| Role | Description |\n| --- | --- |\n'
for role in ROLES:
for role in docs_data.roles():
description = role['description'] or role['label']
ret_data += f'| **{role["label"]}** | {description} |\n'
@@ -507,12 +428,10 @@ def define_env(env):
the models which implement `InvenTreeAuditMixin`), so it can never drift out
of sync with the models actually audited in the source code.
"""
global AUDITED_MODELS
ret_data = '| Category | Model | Model Type | Parent | Notes |\n'
ret_data += '| --- | --- | --- | --- | --- |\n'
for model in AUDITED_MODELS:
for model in docs_data.audited_models():
parent = model['parent']
notes = model['parent_note']
@@ -527,9 +446,7 @@ def define_env(env):
@env.macro
def report_context(type_: Literal['models', 'base'], model: str):
"""Extract information on a particular report context."""
global REPORT_CONTEXT
context = REPORT_CONTEXT.get(type_).get(model)
context = docs_data.report_context().get(type_).get(model)
ret_data = '| Variable | Type | Description |\n| --- | --- | --- |\n'
for k, v in context['context'].items():
@@ -568,9 +485,7 @@ def define_env(env):
`InvenTreeReportMixin`, so there is no manually-maintained per-model heading
list to keep in sync here.
"""
global REPORT_CONTEXT
models = REPORT_CONTEXT.get('models', {})
models = docs_data.report_context().get('models', {})
ret_data = ''
@@ -593,9 +508,7 @@ def define_env(env):
the `export_report_context` management command, so there is no manually-maintained
list of related models to keep in sync here.
"""
global REPORT_CONTEXT
related = REPORT_CONTEXT.get('related_models', {})
related = docs_data.report_context().get('related_models', {})
ret_data = ''
+17 -21
View File
@@ -1,5 +1,14 @@
# Documentation is built with Zensical (https://zensical.org), which reads this
# mkdocs-format config. Build from the repo root with "invoke build-docs --build"
# (or "zensical build -f docs/mkdocs.yml" after running "invoke build-docs").
#
# Zensical does not run mkdocs hooks, so:
# - the values from docs/docs/hooks.py:on_config are injected by docs/main.py
# - the post-build checks (hooks.py:on_post_build) are run by "invoke build-docs"
# - site_url is taken from READTHEDOCS_CANONICAL_URL when building on ReadTheDocs
# Project
site_url: https://inventree.readthedocs.io
site_url: !ENV [READTHEDOCS_CANONICAL_URL, 'https://docs.inventree.org/']
site_name: InvenTree Documentation
site_description: InvenTree - Open Source Inventory Management
site_author: InvenTree
@@ -12,8 +21,7 @@ repo_name: inventree/inventree
# Theme
theme:
name: material
font:
text: Roboto
variant: modern
custom_dir: _includes/overrides
palette:
- media: "(prefers-color-scheme: light)"
@@ -39,7 +47,7 @@ theme:
- header.autohide
- navigation.expand
- navigation.footer
- navigation.indexes
# - navigation.indexes
- navigation.instant
# - navigation.sections
- navigation.tracking
@@ -54,10 +62,10 @@ extra_css:
- stylesheets/brands.css
- stylesheets/regular.css
- stylesheets/solid.css
- stylesheets/fontawesome-svg.css
- stylesheets/bootstrap.css
- stylesheets/splide.min.css
- stylesheets/extra.css
- stylesheets/neoteroi-mkdocs.css
- https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.31.0/dist/tabler-icons.min.css
extra_javascript:
- javascripts/extra.js
@@ -203,7 +211,6 @@ nav:
- Multi Factor Authentication: settings/MFA.md
- SCIM Provisioning: settings/SCIM.md
- Email Settings: settings/email.md
- Experimental Features: settings/experimental.md
- Operations:
- Background Tasks: settings/tasks.md
- Error Logs: settings/logs.md
@@ -304,22 +311,10 @@ nav:
# Plugins
plugins:
- neoteroi.mkdocsoad:
use_pymdownx: true
- include-markdown:
opening_tag: "{!"
closing_tag: "!}"
- search
- mermaid2:
# version: 11.6.0
javascript: javascripts/mermaid.min.js
- git-revision-date-localized
- mkdocs-simple-hooks:
hooks:
on_config: "docs.docs.hooks:on_config"
on_post_build: "docs.docs.hooks:on_post_build"
- macros:
include_dir: docs/_includes
include_dir: _includes
on_error_fail: true
module_name: main
- mkdocstrings:
default_handler: python
@@ -366,7 +361,7 @@ markdown_extensions:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:mermaid2.fence_mermaid_custom
format: !!python/name:pymdownx.superfences.fence_code_format
# - pymdownx.emoji:
# emoji_index: !!python/name:materialx.emoji.twemoji
# emoji_generator: !!python/name:materialx.emoji.to_svg
@@ -412,3 +407,4 @@ extra:
use_directory_urls: true
strict: true
site_dir: site
+3
View File
@@ -11,6 +11,9 @@
429
],
"ignorePatterns": [
{
"pattern": "^\\./schema/[a-z-]+\\.md$"
},
{
"pattern": "http://inventree.localhost"
},
+667
View File
@@ -0,0 +1,667 @@
"""Render sections of the InvenTree OpenAPI schema as static Markdown pages.
This replaces the 'neoteroi.mkdocsoad' plugin, so that the API documentation
is plain Markdown which can be rendered by any static site generator
(mkdocs, zensical, etc), without requiring a build-time plugin.
The rendered pages are generated as a pre-build step (see 'extract_schema.py').
"""
import json
import re
from typing import Any, Optional
HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']
# Placeholder values for generated examples, keyed by OpenAPI 'format'
FORMAT_EXAMPLES = {
'date': '2024-01-01',
'date-time': '2024-01-01T12:00:00Z',
'time': '12:00:00',
'email': 'user@example.com',
'uri': 'https://example.com',
'uuid': '3fa85f64-5717-4562-b3fc-2c963f66afa6',
'decimal': '0.00',
'binary': '<binary>',
'ipv4': '127.0.0.1',
}
# Maximum depth when generating JSON examples (guards against deep nesting)
EXAMPLE_MAX_DEPTH = 5
def schema_anchor(name: str) -> str:
"""Return the HTML anchor for a named component schema."""
return 'schema-' + re.sub(r'[^a-z0-9]+', '-', name.lower()).strip('-')
def operation_anchor(method: str, path: str) -> str:
"""Return the HTML anchor for a single API operation."""
return method.lower() + '-' + re.sub(r'[^a-z0-9]+', '-', path.lower()).strip('-')
def escape_text(text: Any) -> str:
"""Escape free text so it cannot inject HTML into the rendered page.
Markdown formatting (e.g. `code` or *emphasis*) is preserved.
"""
text = '' if text is None else str(text)
return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def escape_cell(text: Any) -> str:
"""Escape text for use within a single Markdown table cell."""
text = escape_text(text).strip()
text = text.replace('|', '\\|')
lines = []
for line in text.splitlines():
line = line.strip()
# Bulleted lists cannot be rendered within a table cell
if line.startswith(('* ', '- ')):
line = '• ' + line[2:]
lines.append(line)
return '<br>'.join(line for line in lines if line)
def inline_code(text: Any) -> str:
"""Wrap a value as inline code, coping with embedded backticks."""
text = str(text)
fence = '``' if '`' in text else '`'
pad = ' ' if '`' in text else ''
return f'{fence}{pad}{text}{pad}{fence}'
class SchemaRenderer:
"""Render API operations and their component schemas to Markdown."""
def __init__(self, spec: dict):
"""Initialize the renderer with the *complete* OpenAPI spec.
The full spec is required so that '$ref' entries can always be resolved,
regardless of which subset of paths is being rendered.
"""
self.spec = spec
self.components = spec.get('components', {})
self.schemas = self.components.get('schemas', {})
# ------------------------------------------------------------------ #
# Reference handling
# ------------------------------------------------------------------ #
@staticmethod
def ref_name(ref: str) -> str:
"""Return the final component name from a '$ref' string."""
return ref.split('/')[-1]
def resolve(self, obj: Any) -> Any:
"""Resolve a '$ref' (to any component type) into the referenced object."""
seen = set()
while isinstance(obj, dict) and '$ref' in obj:
ref = obj['$ref']
if ref in seen or not ref.startswith('#/'):
break
seen.add(ref)
node = self.spec
for part in ref[2:].split('/'):
node = node.get(part, {}) if isinstance(node, dict) else {}
obj = node
return obj
def collect_schema_refs(self, obj: Any, found: Optional[set] = None) -> set:
"""Find all component schemas referenced (transitively) by an object.
References to other component types (e.g. responses or parameters)
are followed, so that the schemas they use are also included.
"""
found = set() if found is None else found
stack = [obj]
visited = set()
while stack:
node = stack.pop()
if isinstance(node, list):
stack.extend(node)
continue
if not isinstance(node, dict):
continue
ref = node.get('$ref')
if isinstance(ref, str) and ref not in visited:
visited.add(ref)
if ref.startswith('#/components/schemas/'):
found.add(self.ref_name(ref))
stack.append(self.resolve(node))
stack.extend(v for k, v in node.items() if k != '$ref')
return found
# ------------------------------------------------------------------ #
# Type descriptions
# ------------------------------------------------------------------ #
def type_str(self, schema: Any) -> str:
"""Return a short (Markdown) description of a schema's type."""
if not isinstance(schema, dict):
return ''
if '$ref' in schema:
ref = schema['$ref']
if ref.startswith('#/components/schemas/'):
name = self.ref_name(ref)
return f'[{name}](#{schema_anchor(name)})'
return self.type_str(self.resolve(schema))
for key, joiner in (('allOf', ' & '), ('oneOf', ' or '), ('anyOf', ' or ')):
if key in schema:
options = [self.type_str(s) for s in schema[key]]
return joiner.join(o for o in options if o) or 'any'
schema_type = schema.get('type')
if schema_type == 'array':
return f'array[{self.type_str(schema.get("items", {})) or "any"}]'
if schema_type == 'object' or (not schema_type and 'properties' in schema):
extra = schema.get('additionalProperties')
if isinstance(extra, dict) and extra:
return f'object[string, {self.type_str(extra) or "any"}]'
return 'object'
if not schema_type:
return 'any'
if fmt := schema.get('format'):
return f'{schema_type}({fmt})'
return schema_type
def merged(self, schema: dict) -> dict:
"""Flatten 'allOf' wrappers so that flags such as 'readOnly' are visible."""
if not isinstance(schema, dict):
return {}
result = {k: v for k, v in schema.items() if k != 'allOf'}
for sub in schema.get('allOf', []):
sub = self.resolve(sub)
if isinstance(sub, dict):
for k, v in sub.items():
result.setdefault(k, v)
return result
def constraints(self, schema: dict, description: str = '') -> list[str]:
"""Return a list of human readable constraints for a schema.
Arguments:
schema: The schema to describe
description: Accompanying description text. Enum values are not
repeated if the description already lists them all.
"""
notes = []
enum_listed = all(
f'`{v}`' in (description or '') for v in schema.get('enum', []) if v
)
if 'enum' in schema and not enum_listed:
values = ', '.join(inline_code(json.dumps(v)) for v in schema['enum'])
notes.append(f'One of: {values}')
for key, label in (
('minLength', 'Min length'),
('maxLength', 'Max length'),
('minimum', 'Minimum'),
('maximum', 'Maximum'),
('minItems', 'Min items'),
('maxItems', 'Max items'),
):
if key in schema:
notes.append(f'{label}: {inline_code(schema[key])}')
if 'pattern' in schema:
notes.append(f'Pattern: {inline_code(schema["pattern"])}')
if 'default' in schema:
notes.append(f'Default: {inline_code(json.dumps(schema["default"]))}')
return notes
# ------------------------------------------------------------------ #
# Example generation
# ------------------------------------------------------------------ #
def example(
self, schema: Any, mode: str = 'response', depth: int = 0, stack=()
) -> Any:
"""Generate an example value for a schema.
Arguments:
schema: The schema to generate an example for
mode: 'request' (omit read-only fields) or 'response' (omit write-only fields)
depth: Current nesting depth
stack: Component names currently being expanded (cycle protection)
"""
if not isinstance(schema, dict) or depth > EXAMPLE_MAX_DEPTH:
return None
if '$ref' in schema:
name = self.ref_name(schema['$ref'])
if name in stack:
return {}
return self.example(self.resolve(schema), mode, depth, (*stack, name))
if 'example' in schema:
return schema['example']
if 'allOf' in schema:
result = {}
for sub in schema['allOf']:
value = self.example(sub, mode, depth, stack)
if isinstance(value, dict):
result.update(value)
elif value is not None:
return value
return result
for key in ('oneOf', 'anyOf'):
if key in schema:
# Prefer the first option which is not a 'null' or 'blank' variant
for sub in schema[key]:
value = self.example(sub, mode, depth, stack)
if value not in (None, ''):
return value
return None
if 'default' in schema:
return schema['default']
if 'enum' in schema:
options = [v for v in schema['enum'] if v not in (None, '')]
return options[0] if options else None
schema_type = schema.get('type')
if schema_type == 'object' or 'properties' in schema:
result = {}
for name, prop in schema.get('properties', {}).items():
flags = self.merged(prop)
if mode == 'request' and flags.get('readOnly'):
continue
if mode == 'response' and flags.get('writeOnly'):
continue
result[name] = self.example(prop, mode, depth + 1, stack)
return result
if schema_type == 'array':
item = self.example(schema.get('items', {}), mode, depth + 1, stack)
return [] if item is None else [item]
if schema_type == 'string':
return FORMAT_EXAMPLES.get(schema.get('format'), 'string')
if schema_type == 'integer':
return 0
if schema_type == 'number':
return 0.0
if schema_type == 'boolean':
return False
return None
def example_block(self, schema: dict, mode: str, title: str) -> list[str]:
"""Render a collapsible JSON example for the given schema."""
value = self.example(schema, mode)
if value is None:
return []
body = json.dumps(value, indent=2)
lines = [f'??? example "{title}"', ' ```json']
lines += [' ' + line for line in body.splitlines()]
lines += [' ```', '']
return lines
# ------------------------------------------------------------------ #
# Operations
# ------------------------------------------------------------------ #
@staticmethod
def security_names(security: Optional[list]) -> tuple:
"""Return a sorted tuple of security scheme names for an operation."""
if not security:
return ()
names = set()
for requirement in security:
names.update(requirement.keys())
return tuple(sorted(names))
def render_security(self, names: tuple) -> str:
"""Render a list of security schemes as Markdown."""
if not names:
return 'No authentication required'
return ', '.join(inline_code(n) for n in names)
def render_parameters(self, parameters: list) -> list[str]:
"""Render the parameters of an operation as a table."""
if not parameters:
return []
lines = [
'**Parameters**',
'',
'| Name | In | Type | Required | Description |',
'| --- | --- | --- | --- | --- |',
]
for param in parameters:
param = self.resolve(param)
schema = param.get('schema', {})
notes = [escape_cell(param.get('description', ''))]
notes += self.constraints(
self.merged(self.resolve(schema)), param.get('description', '')
)
description = '<br>'.join(n for n in notes if n)
lines.append(
f'| {inline_code(param.get("name", ""))} '
f'| {param.get("in", "")} '
f'| {self.type_str(schema)} '
f'| {"Yes" if param.get("required") else "No"} '
f'| {description} |'
)
return [*lines, '']
def render_request_body(self, body: dict) -> list[str]:
"""Render the request body of an operation."""
body = self.resolve(body)
content = body.get('content', {})
if not content:
return []
content_types = ', '.join(inline_code(c) for c in content)
required = ' (required)' if body.get('required') else ''
# Prefer the JSON representation for documentation purposes
media = content.get('application/json') or next(iter(content.values()))
schema = media.get('schema', {})
lines = [
f'**Request body**{required}: {self.type_str(schema)}',
'',
f'Content types: {content_types}',
'',
]
lines += self.example_block(schema, 'request', 'Example request body')
return lines
def render_responses(self, responses: dict) -> list[str]:
"""Render the responses of an operation."""
if not responses:
return []
lines = [
'**Responses**',
'',
'| Status | Description | Schema |',
'| --- | --- | --- |',
]
examples = []
for status, response in responses.items():
response = self.resolve(response)
content = response.get('content', {})
media = content.get('application/json') or (
next(iter(content.values())) if content else {}
)
schema = media.get('schema', {}) if media else {}
lines.append(
f'| {inline_code(status)} '
f'| {escape_cell(response.get("description", ""))} '
f'| {self.type_str(schema) if schema else ""} |'
)
# Provide an example for successful JSON responses only
if schema and str(status).startswith('2') and not examples:
examples = self.example_block(
schema, 'response', f'Example response ({status})'
)
return [*lines, '', *examples]
def render_operation(
self, path: str, method: str, operation: dict, default_security: tuple
) -> list[str]:
"""Render a single API operation (path + method)."""
anchor = operation_anchor(method, path)
label = method.upper()
lines = [
f'### <span class="api-method api-method-{method}">{label}</span> '
f'{inline_code(path)} {{#{anchor}}}',
'',
]
if operation.get('deprecated'):
lines += [
'!!! warning "Deprecated"',
' This endpoint is deprecated.',
'',
]
if summary := operation.get('summary'):
lines += [f'**{escape_text(summary).strip()}**', '']
if description := operation.get('description'):
lines += [escape_text(description).strip(), '']
security = self.security_names(operation.get('security'))
if security != default_security:
lines += [f'**Authentication**: {self.render_security(security)}', '']
lines += self.render_parameters(operation.get('parameters', []))
lines += self.render_request_body(operation.get('requestBody', {}))
lines += self.render_responses(operation.get('responses', {}))
return lines
# ------------------------------------------------------------------ #
# Component schemas
# ------------------------------------------------------------------ #
def render_schema(self, name: str) -> list[str]:
"""Render a single named component schema."""
schema = self.schemas.get(name, {})
lines = [f'### {name} {{#{schema_anchor(name)}}}', '']
if description := schema.get('description'):
lines += [escape_text(description).strip(), '']
properties = schema.get('properties', {})
if properties:
required = set(schema.get('required', []))
lines += [
'| Field | Type | Required | Description |',
'| --- | --- | --- | --- |',
]
for field, prop in properties.items():
flags = self.merged(prop)
resolved = self.merged(self.resolve(prop))
notes = []
badges = []
if flags.get('readOnly'):
badges.append('read-only')
if flags.get('writeOnly'):
badges.append('write-only')
if flags.get('nullable'):
badges.append('nullable')
if badges:
notes.append(' '.join(f'*{b}*' for b in badges))
if title := flags.get('title'):
if title.lower() != field.replace('_', ' ').lower():
notes.append(f'**{escape_cell(title)}**')
field_description = flags.get('description') or resolved.get(
'description'
)
notes.append(escape_cell(field_description))
# Constraints of referenced schemas are documented on that schema
notes += self.constraints(flags, field_description)
lines.append(
f'| {inline_code(field)} '
f'| {self.type_str(prop)} '
f'| {"Yes" if field in required else "No"} '
f'| {"<br>".join(n for n in notes if n)} |'
)
lines.append('')
elif 'enum' in schema:
base_type = self.type_str({k: v for k, v in schema.items() if k != 'enum'})
lines += [f'Type: {base_type}', '']
# Only list the values if the description does not already do so
if self.constraints({'enum': schema['enum']}, description or ''):
lines += ['| Value |', '| --- |']
lines += [f'| {inline_code(json.dumps(v))} |' for v in schema['enum']]
lines.append('')
else:
lines += [f'Type: {self.type_str(schema)}', '']
if notes := self.constraints(schema):
lines += ['<br>'.join(notes), '']
return lines
# ------------------------------------------------------------------ #
# Page rendering
# ------------------------------------------------------------------ #
def default_security(self, paths: dict) -> tuple:
"""Return the most common set of security schemes across the given paths."""
counts = {}
for methods in paths.values():
for method, operation in methods.items():
if method in HTTP_METHODS:
key = self.security_names(operation.get('security'))
counts[key] = counts.get(key, 0) + 1
if not counts:
return ()
return max(counts, key=counts.get)
def render_page(self, title: str, paths: dict) -> str:
"""Render a full documentation page for a subset of API paths.
Arguments:
title: The title of this section of the API
paths: Dict of {path: {method: operation}} items to render
"""
info = self.spec.get('info', {})
default_security = self.default_security(paths)
lines = [
'---',
f'title: {json.dumps(title)}',
# Schema text must never be interpreted as Jinja2 template code
'render_macros: false',
'---',
'',
f'The *{escape_text(title)}* section of the InvenTree API schema is documented below.',
'',
f'!!! info "API Version {escape_text(info.get("version", ""))}"',
' This page was generated automatically from the InvenTree API schema.',
'',
]
if default_security:
lines += [
'## Authentication',
'',
'Unless otherwise noted, endpoints on this page accept the following authentication methods: '
+ self.render_security(default_security)
+ '. Refer to the [API documentation](../index.md#authentication) for more information.',
'',
]
lines += ['## Endpoints', '']
referenced = set()
for path in sorted(paths):
methods = paths[path]
# Parameters may also be declared at the path level (shared by all methods)
path_params = methods.get('parameters', [])
for method in HTTP_METHODS:
if operation := methods.get(method):
if path_params:
operation = {
**operation,
'parameters': path_params + operation.get('parameters', []),
}
lines += self.render_operation(
path, method, operation, default_security
)
self.collect_schema_refs(operation, referenced)
if referenced:
lines += ['## Schemas', '']
for name in sorted(referenced, key=str.lower):
lines += self.render_schema(name)
return '\n'.join(lines).rstrip() + '\n'
+1 -10
View File
@@ -1,11 +1,2 @@
properdocs==1.6.7
mkdocs<2.0
mkdocs-macros-plugin>=0.7,<2.0
mkdocs-material>=9.7.7,<10.0
mkdocs-git-revision-date-localized-plugin>=1.6.0,<2.0
mkdocs-redirects
mkdocs-simple-hooks
mkdocs-include-markdown-plugin
neoteroi-mkdocs
zensical
mkdocstrings[python]
mkdocs-mermaid2-plugin
+92 -414
View File
@@ -1,299 +1,27 @@
# This file was autogenerated by uv via the following command:
# uv pip compile docs/requirements.in -o docs/requirements.txt -c src/backend/requirements.txt
anyio==4.15.1 \
--hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \
--hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94
# via httpx
babel==2.18.0 \
--hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \
--hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35
# via
# -c src/backend/requirements.txt
# mkdocs-git-revision-date-localized-plugin
# mkdocs-material
backrefs==8.1 \
--hash=sha256:11f61846e446fa8637af2f5c073f1083aa58dbb7460b019fd51689f564e44f7e \
--hash=sha256:1e857c32e91b8fe46ddb359ac21021a37b090fa4239566a61e9856123e334041 \
--hash=sha256:438bd38dc7fa0cded52786a1351a2dad9efc2cda95d047284c41d916a22312a8 \
--hash=sha256:9c8fe3e2eaae4301a9ce787341d986e5ba25a887d89da6de0de5d58eca26a949 \
--hash=sha256:b472c76082769f86a97ffccfd1f2599b2b706610c6154955664fffe67e909035 \
--hash=sha256:db577dbae1c61187534d35fc9bfbd0c01158fff6239a66fecf7590e5b17879a9 \
--hash=sha256:f86d9312df3ebae1241758af80809a319cafa1d8f32189edfb242c9aa55573b6
# via mkdocs-material
beautifulsoup4==4.15.0 \
--hash=sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7 \
--hash=sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9
# via mkdocs-mermaid2-plugin
bracex==3.2 \
--hash=sha256:0a3d40bfae4caae4d3bfd0ea715874799fab711c38fd4012cc35343e815405cd \
--hash=sha256:8f3e908a452f66707e1d6d91b3d7d7d4c649d248df18537c0dc710aeb9c30d87
# via wcmatch
certifi==2026.10.23 \
--hash=sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd \
--hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
# via
# -c src/backend/requirements.txt
# httpcore
# httpx
# requests
charset-normalizer==3.5.2 \
--hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \
--hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \
--hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \
--hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \
--hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \
--hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \
--hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \
--hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \
--hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \
--hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \
--hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \
--hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \
--hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \
--hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \
--hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \
--hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \
--hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \
--hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \
--hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \
--hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \
--hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \
--hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \
--hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \
--hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \
--hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \
--hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \
--hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \
--hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \
--hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \
--hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \
--hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \
--hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \
--hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \
--hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \
--hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \
--hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \
--hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \
--hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \
--hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \
--hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \
--hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \
--hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \
--hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \
--hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \
--hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \
--hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \
--hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \
--hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \
--hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \
--hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \
--hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \
--hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \
--hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \
--hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \
--hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \
--hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \
--hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \
--hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \
--hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \
--hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \
--hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \
--hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \
--hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \
--hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \
--hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \
--hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \
--hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \
--hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \
--hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \
--hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \
--hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \
--hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \
--hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \
--hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \
--hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \
--hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \
--hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \
--hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \
--hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \
--hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \
--hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \
--hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \
--hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \
--hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \
--hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \
--hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \
--hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \
--hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \
--hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \
--hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \
--hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \
--hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \
--hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \
--hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \
--hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \
--hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \
--hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \
--hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \
--hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \
--hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \
--hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \
--hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \
--hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \
--hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \
--hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \
--hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \
--hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \
--hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \
--hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \
--hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \
--hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \
--hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \
--hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \
--hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \
--hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \
--hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \
--hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \
--hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \
--hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \
--hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \
--hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \
--hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \
--hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \
--hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \
--hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \
--hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \
--hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \
--hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \
--hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \
--hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \
--hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \
--hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \
--hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \
--hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \
--hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \
--hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \
--hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \
--hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \
--hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \
--hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \
--hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \
--hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \
--hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \
--hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \
--hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \
--hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \
--hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \
--hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \
--hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \
--hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \
--hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \
--hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \
--hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \
--hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \
--hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \
--hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \
--hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \
--hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \
--hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \
--hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \
--hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \
--hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \
--hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \
--hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \
--hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \
--hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \
--hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \
--hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \
--hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \
--hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \
--hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \
--hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f
# via
# -c src/backend/requirements.txt
# requests
click==8.5.0 \
--hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \
--hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34
# via
# mkdocs
# neoteroi-mkdocs
# properdocs
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via mkdocs-material
editorconfig==0.17.1 \
--hash=sha256:1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82 \
--hash=sha256:23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745
# via jsbeautifier
essentials==1.1.9 \
--hash=sha256:71ef161e0e27ef77cd6f5fc05e0b8688a575fcab870c01c95940f832e321dfbb \
--hash=sha256:7fbea3a518cbeafe5374fb7e2ea2c15a109e8a7fd1eaab62ae87cbd1b3b1e8d0
# via essentials-openapi
essentials-openapi==1.4.0 \
--hash=sha256:578c81501ccf6d18c0839d60636214fbd051f0ef37f1d207d4e3c92de2aac008 \
--hash=sha256:86d879c32734248ad52482a90ee89a32883bce348b4edd323c01556b505b45b9
# via neoteroi-mkdocs
# zensical
ghp-import==2.1.0 \
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
--hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
# via
# mkdocs
# properdocs
gitdb==4.0.12 \
--hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \
--hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf
# via gitpython
gitpython==3.2.1 \
--hash=sha256:0f1229ec5c057bdabdeeda8bda4f17bda679ab33dbccc00f24e6a480553c8613 \
--hash=sha256:d66b0832aa8755c1a84892937282df0befb16c97e5a64aeebd68be9c5e323f53
# via mkdocs-git-revision-date-localized-plugin
# via mkdocs
griffelib==2.4.1 \
--hash=sha256:40c028dd2e623f464423f0e11e3c553158f1cf4f1368239a370534d73e1407ff \
--hash=sha256:76bc562f936ad1ce198d2cf6d0731bd5c92a7d934b69ef828ef22ee0ffdb62ce
# via mkdocstrings-python
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via httpcore
hjson==3.1.0 \
--hash=sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75 \
--hash=sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89
# via
# mkdocs-macros-plugin
# super-collections
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via neoteroi-mkdocs
idna==3.20 \
--hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \
--hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
# via
# -c src/backend/requirements.txt
# anyio
# httpx
# requests
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
# via
# -c src/backend/requirements.txt
# mkdocs
# mkdocs-macros-plugin
# mkdocs-material
# mkdocstrings
# neoteroi-mkdocs
# properdocs
jsbeautifier==2.0.3 \
--hash=sha256:9579d4e9dbaa00383f3efdff4c98c8140bb85ba319398e8b97cdaba27abd6ba3 \
--hash=sha256:f0190e279a2cdb827556ada63f41c9c63c11f8116ee06e264b24aa50311cbee3
# via mkdocs-mermaid2-plugin
# zensical
markdown==3.11.2 \
--hash=sha256:87bf5d90f2d2882e2c02fcda182c2800456b985bff38913867c49706481514b0 \
--hash=sha256:f80390747a2a0ff2ba54da074ce4f862f72a6685f608cd2637b646a6d83993f5
@@ -301,14 +29,9 @@ markdown==3.11.2 \
# -c src/backend/requirements.txt
# mkdocs
# mkdocs-autorefs
# mkdocs-material
# mkdocstrings
# properdocs
# pymdown-extensions
markdown-it-py==4.2.0 \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
# via rich
# zensical
markupsafe==3.0.4 \
--hash=sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98 \
--hash=sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002 \
@@ -463,16 +186,10 @@ markupsafe==3.0.4 \
--hash=sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3
# via
# -c src/backend/requirements.txt
# essentials-openapi
# jinja2
# mkdocs
# mkdocs-autorefs
# mkdocstrings
# properdocs
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
# via markdown-it-py
mergedeep==1.3.4 \
--hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \
--hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307
@@ -483,17 +200,8 @@ mkdocs==1.6.1 \
--hash=sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2 \
--hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e
# via
# -r docs/requirements.in
# mkdocs-autorefs
# mkdocs-git-revision-date-localized-plugin
# mkdocs-include-markdown-plugin
# mkdocs-macros-plugin
# mkdocs-material
# mkdocs-mermaid2-plugin
# mkdocs-redirects
# mkdocs-simple-hooks
# mkdocstrings
# neoteroi-mkdocs
mkdocs-autorefs==1.4.5 \
--hash=sha256:68ee484e0800f3ba50cda1bc4a94293adbdfcc010243c322b3f5d66e15d06770 \
--hash=sha256:856f75b327b7717923ce06267094650689a510a748c48657f5333965950c2788
@@ -504,38 +212,6 @@ mkdocs-get-deps==0.2.2 \
--hash=sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1 \
--hash=sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650
# via mkdocs
mkdocs-git-revision-date-localized-plugin==1.6.2 \
--hash=sha256:9db79422039f856e582df62b360f9b9bde0f1b7c978d253bdb2a1b73a18b7774 \
--hash=sha256:9f38e5cfef3d180f1c479a7190d72617267a5d79d8159b12e3ae39c80f6e7488
# via -r docs/requirements.in
mkdocs-include-markdown-plugin==7.3.0 \
--hash=sha256:2800126746452e31c2e321bbd43c8190b356e0de353e20cbc16a34a3c3d6796c \
--hash=sha256:5b5c99b5d3c9b9ce0114a9e60353bbafb6be53a26c2d3b74ec6b767a7a8e55ca
# via -r docs/requirements.in
mkdocs-macros-plugin==1.5.0 \
--hash=sha256:12aa45ce7ecb7a445c66b9f649f3dd05e9b92e8af6bc65e4acd91d26f878c01f \
--hash=sha256:c10fabd812bf50f9170609d0ed518e54f1f0e12c334ac29141723a83c881dd6f
# via -r docs/requirements.in
mkdocs-material==9.8.0 \
--hash=sha256:19304a7263c5700428227122b3b2699aa73f432c9de8816876adaec7d7293ce3 \
--hash=sha256:1ed4c5edaf405a3fb62e55288b8fdff592971729361548b1ffa757bf9849fb81
# via -r docs/requirements.in
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
--hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31
# via mkdocs-material
mkdocs-mermaid2-plugin==1.2.3 \
--hash=sha256:33f60c582be623ed53829a96e19284fc7f1b74a1dbae78d4d2e47fe00c3e190d \
--hash=sha256:fb6f901d53e5191e93db78f93f219cad926ccc4d51e176271ca5161b6cc5368c
# via -r docs/requirements.in
mkdocs-redirects==1.2.3 \
--hash=sha256:5e980330999299729a2d6a125347d1af78023d68a23681a4de3053ce7dfe2e51 \
--hash=sha256:ec7312fff462d03ec16395d0c001006a418f8d0c21cdf2b47ff11cf839dc3ce0
# via -r docs/requirements.in
mkdocs-simple-hooks==0.1.5 \
--hash=sha256:dddbdf151a18723c9302a133e5cf79538be8eb9d274e8e07d2ac3ac34890837c \
--hash=sha256:efeabdbb98b0850a909adee285f3404535117159d5cb3a34f541d6eaa644d50a
# via -r docs/requirements.in
mkdocstrings[python]==1.0.8 \
--hash=sha256:4b03ed2b863009f1153f0d04aacb22a85b92afc28a0636d85717c92eecca638c \
--hash=sha256:b50fc326230906425c82ee9ec59b80e5db00892b2b593d4d0b841a0d4acf537b
@@ -546,63 +222,40 @@ mkdocstrings-python==2.0.10 \
--hash=sha256:4a3aaeebb25ba5ae57a8f9e75432fe4571984889602f2b2f7d672faa760e9062 \
--hash=sha256:5e34ae56b2f20a57807ee479d603ddc5ec7cc6d2252c4c177754627ef2d3b833
# via mkdocstrings
neoteroi-mkdocs==1.2.0 \
--hash=sha256:58e25cb1b9db093ffa8d12bdb33264bf567cac30fb964b56e0a493efa749ad6e \
--hash=sha256:91b6aa95a4e46c9bb93e00e021d2044cb0c7d80c0b4600434ff8f440d613a0a8
# via -r docs/requirements.in
packaging==26.3 \
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
# via
# -c src/backend/requirements.txt
# mkdocs
# mkdocs-git-revision-date-localized-plugin
# mkdocs-macros-plugin
# properdocs
paginate==0.5.7 \
--hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
--hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
# via mkdocs-material
pathspec==1.1.1 \
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
# via
# mkdocs
# mkdocs-macros-plugin
# properdocs
# zensical
platformdirs==4.14.5 \
--hash=sha256:32e9921db7385aa8169bee4fe9501923e5715ba9fb7ea3b9c69b0d8e0639f7a1 \
--hash=sha256:8e778c6fa421516f4d3369449f8df3b0e1b72c0e0005fe88e3ec772d0a14e750
# via
# -c src/backend/requirements.txt
# mkdocs-get-deps
# properdocs
properdocs==1.6.7 \
--hash=sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd \
--hash=sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e
# via
# -r docs/requirements.in
# mkdocs-redirects
pygments==2.21.0 \
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
# via
# mkdocs-material
# rich
# via zensical
pymdown-extensions==12.3 \
--hash=sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74 \
--hash=sha256:a7f2a4e0bc60c6e6babfa7922cfb3b01cd640eb00fb208f3c582e12bae00ea67
# via
# mkdocs-material
# mkdocs-mermaid2-plugin
# mkdocstrings
# zensical
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
# via
# -c src/backend/requirements.txt
# ghp-import
# mkdocs-macros-plugin
pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
@@ -679,72 +332,88 @@ pyyaml==6.0.3 \
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
# via
# -c src/backend/requirements.txt
# essentials-openapi
# mkdocs
# mkdocs-get-deps
# mkdocs-macros-plugin
# properdocs
# pymdown-extensions
# pyyaml-env-tag
# zensical
pyyaml-env-tag==1.1 \
--hash=sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 \
--hash=sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff
# via
# mkdocs
# properdocs
requests==2.34.2 \
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
# via
# -c src/backend/requirements.txt
# mkdocs-macros-plugin
# mkdocs-material
# mkdocs-mermaid2-plugin
rich==15.0.0 \
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
# via neoteroi-mkdocs
setuptools==84.2.1 \
--hash=sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc \
--hash=sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2
# via
# -c src/backend/requirements.txt
# mkdocs-mermaid2-plugin
# via mkdocs
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via
# -c src/backend/requirements.txt
# python-dateutil
smmap==5.0.3 \
--hash=sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c \
--hash=sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f
# via gitdb
soupsieve==4.0 \
--hash=sha256:5e7494bfce90e06676cfb4c5c2cf22243e98e5cf24646bb1ecdda46d318550a0 \
--hash=sha256:ccd6470b256394ac44b08b86fd94b30a17d335a5335c8a3d15565f7b03d3c8cf
# via beautifulsoup4
super-collections==0.6.2 \
--hash=sha256:0c8d8abacd9fad2c7c1c715f036c29f5db213f8cac65f24d45ecba12b4da187a \
--hash=sha256:291b74d26299e9051d69ad9d89e61b07b6646f86a57a2f5ab3063d206eee9c56
# via mkdocs-macros-plugin
termcolor==3.3.0 \
--hash=sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5 \
--hash=sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5
# via mkdocs-macros-plugin
typing-extensions==4.16.0 \
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
# via
# -c src/backend/requirements.txt
# anyio
# beautifulsoup4
urllib3==2.8.0 \
--hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \
--hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63
# via
# -c src/backend/requirements.txt
# requests
tomli==2.5.0 \
--hash=sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea \
--hash=sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd \
--hash=sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0 \
--hash=sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391 \
--hash=sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df \
--hash=sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9 \
--hash=sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066 \
--hash=sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f \
--hash=sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57 \
--hash=sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6 \
--hash=sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b \
--hash=sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3 \
--hash=sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043 \
--hash=sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01 \
--hash=sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646 \
--hash=sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859 \
--hash=sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b \
--hash=sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e \
--hash=sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc \
--hash=sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5 \
--hash=sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0 \
--hash=sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb \
--hash=sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84 \
--hash=sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6 \
--hash=sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b \
--hash=sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b \
--hash=sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52 \
--hash=sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd \
--hash=sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75 \
--hash=sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1 \
--hash=sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b \
--hash=sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142 \
--hash=sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03 \
--hash=sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea \
--hash=sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885 \
--hash=sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374 \
--hash=sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3 \
--hash=sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276 \
--hash=sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b \
--hash=sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc \
--hash=sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68 \
--hash=sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a \
--hash=sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f \
--hash=sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b \
--hash=sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7 \
--hash=sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0 \
--hash=sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb \
--hash=sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7 \
--hash=sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545 \
--hash=sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8 \
--hash=sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980 \
--hash=sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7 \
--hash=sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105 \
--hash=sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5 \
--hash=sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56 \
--hash=sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d \
--hash=sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2 \
--hash=sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4 \
--hash=sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7 \
--hash=sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef \
--hash=sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1 \
--hash=sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571 \
--hash=sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a \
--hash=sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442 \
--hash=sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc
# via zensical
watchdog==7.0.0 \
--hash=sha256:03a2c85c63cce3cbda9ab6ce21ca401e95b6f681cc6f70316f4f882b3c8f1315 \
--hash=sha256:047f5f823286add71294ff1a186c9fc89fea4a2afa2a1d91513b64dbf43fd473 \
@@ -786,10 +455,19 @@ watchdog==7.0.0 \
--hash=sha256:ea19e8857af78d23b45f80c316e502159443fba2f382b1361ab7d96e3848bb01 \
--hash=sha256:edbfd843840fa8a750d79ed69e65ab67965748f9ccfc56ff957e1cee4a8cf796 \
--hash=sha256:f06a2d489adcbd6e1f1faa6eb74b167d1f78fc4dc3bccb309a09d0e2cea1fc8e
# via
# mkdocs
# properdocs
wcmatch==11.2 \
--hash=sha256:1605bde546fbde4c063628c7d89eda486909fd4337e51d73a873987fbcb2554c \
--hash=sha256:952bab83c75c233836007a69550a3a8753cf06964c1eb3b84ff11ad5bff1c272
# via mkdocs-include-markdown-plugin
# via mkdocs
zensical==0.1.1 \
--hash=sha256:0405d2891dca76a6fb42fae8141dba93a95f31905148940a9f13883bf1ccfe43 \
--hash=sha256:1316ba8c5e18b9e46823b0a10105585a582a6e85d2df764b3b65f3de4d1a711f \
--hash=sha256:267a8f038ca2c720414c0c5810e29962999fbf799369c06a9530b8c702a28f92 \
--hash=sha256:332e74e3fe91cd8476369217e28ab5d4692486da32d0177c8bcb93c972f0cc8b \
--hash=sha256:39f40e800f89ce9253839dcba1a06fdd160ff00ac600ab79ae6f18e6a42ee8f7 \
--hash=sha256:547d2c5c68db7db1faa4394d99ab81f5e9eed6eefe528eb139ff4ea196b2a4fb \
--hash=sha256:d5cff293282dd27bd511f7bd33db8088fd450de4c1611db842a411466140f637 \
--hash=sha256:dd4082e317ac5784c50e21cbe209fd1881a351da6ae4e81cc700d6673d5312f3 \
--hash=sha256:e11abaf2669c5ee780a145d114e8c3db4cd81cd3fdfc8a4513a324301fefd113
# via -r docs/requirements.in
zensical-ui==0.0.42 \
--hash=sha256:db4f532b45d7f8200d08d5fb8e7945c69dd738eed49417008b847572a8e3ac8d \
--hash=sha256:eb824d9c971debc2031837e8d287adf81f5d9ce06304cfbd33fc88ff833abbf3
# via zensical
+11 -13
View File
@@ -1,19 +1,17 @@
version: 2
mkdocs:
configuration: docs/mkdocs.yml
python:
install:
- requirements: docs/requirements.txt
- requirements: src/backend/requirements.txt
# The documentation is built with Zensical, which ReadTheDocs does not support
# natively, so a custom build is used. The output must be placed in
# $READTHEDOCS_OUTPUT/html (see https://docs.readthedocs.com/platform/stable/build-customization.html)
build:
os: "ubuntu-24.04"
tools:
python: "3.12"
jobs:
post_install:
- pip install -U invoke
- echo "Generating API schema file"
- invoke build-docs
commands:
- pip install -r docs/requirements.txt
- pip install -r src/backend/requirements.txt
- pip install -U invoke
# Generate the API schema and settings definitions, build the site and run the post-build checks
- invoke build-docs --build
- mkdir -p $READTHEDOCS_OUTPUT/html
- cp -r docs/site/. $READTHEDOCS_OUTPUT/html/
+26 -6
View File
@@ -111,10 +111,14 @@ ARGV_COMMANDS: dict[str, tuple[str, ...]] = {
'schema',
'spectactular',
# schema adjacent calls
'export_settings_definitions',
'export_tags',
'export_audited_models',
'export_filters',
'export_fsm',
'export_report_context',
'export_roles',
'export_settings_definitions',
'export_status_codes',
'export_tags',
),
# Commands which must not touch the database during the app 'ready' phase
'database_excluded': (
@@ -188,14 +192,32 @@ def isCollectingPlugins():
return cmd_context['collect_plugins']
# Prefix of the RuntimeWarning Django emits when the database is queried before the app registry is ready
APPS_NOT_READY_WARNING_MSG = (
'Accessing the database during app initialization is discouraged'
)
# This variable is used to cache the result of the isGeneratingSchema function, to prevent multiple executions of the same checks
_IS_GENERATING_SCHEMA: bool | None = None
def _setGeneratingSchema(value: bool):
"""Set the value of the isGeneratingSchema variable."""
"""Set the value of the isGeneratingSchema variable.
When schema generation is detected, the Django 'APPS_NOT_READY' database warning
is suppressed for the remainder of the process. Schema generation (and the
'schema adjacent' export commands) intentionally query the database during model
import (e.g. to document custom status codes), so this warning is expected noise.
These are one-shot CLI processes, so a process-wide filter is safe here.
"""
global _IS_GENERATING_SCHEMA
_IS_GENERATING_SCHEMA = value
if value:
warnings.filterwarnings(
'ignore', message=APPS_NOT_READY_WARNING_MSG, category=RuntimeWarning
)
return value
@@ -360,9 +382,7 @@ def ignore_ready_warning(func):
def wrapper(*args, **kwargs):
with warnings.catch_warnings():
warnings.filterwarnings(
'ignore',
message='Accessing the database during app initialization is discouraged',
category=RuntimeWarning,
'ignore', message=APPS_NOT_READY_WARNING_MSG, category=RuntimeWarning
)
return func(*args, **kwargs)
+28 -8
View File
@@ -2617,6 +2617,22 @@ def doc_schema(c):
run(c, 'python docs/extract_schema.py docs/generated/schema.yml')
# Configuration file for building the documentation with zensical
DOCS_CONFIG = 'docs/mkdocs.yml'
def doc_checks(c):
"""Check that all settings and status codes are documented.
Zensical does not support mkdocs hooks, so the 'on_post_build' hook
is run separately, after the documentation has been built.
"""
run(
c,
'python -c "import sys; sys.path.insert(0, \'docs/docs\'); import hooks; hooks.on_post_build()"',
)
@task(
help={
'address': 'Host and port to run the server on (default: localhost:8080)',
@@ -2630,7 +2646,7 @@ def docs_server(
compile_schema: bool = False,
export_settings: bool = True,
):
"""Start a local properdocs server to view the documentation."""
"""Start a local zensical server to view the documentation."""
# Extract settings definitions
if export_settings:
export_definitions(c, basedir='docs')
@@ -2638,23 +2654,27 @@ def docs_server(
if compile_schema:
doc_schema(c)
run(c, f'properdocs serve -a {address} -f docs/mkdocs.yml')
run(c, f'zensical serve -a {address} -f {DOCS_CONFIG}')
@task(
help={'mkdocs': 'Build the documentation using mkdocs at the end (default: False)'}
help={
'build': 'Build the documentation using zensical at the end (default: False)',
'clean': 'Clear the zensical build cache first (default: False)',
}
)
def build_docs(c, mkdocs=False):
"""Build the required documents for building the docs. Optionally build the documentation using mkdocs."""
def build_docs(c, build=False, clean=False):
"""Build the required documents for building the docs. Optionally build the documentation using zensical."""
migrate(c)
export_definitions(c, basedir='docs')
doc_schema(c)
if mkdocs:
run(c, 'properdocs build -f docs/mkdocs.yml')
if build:
run(c, f'zensical build {"--clean " if clean else ""}-f {DOCS_CONFIG}')
doc_checks(c)
info('Documentation build complete')
else:
info('Documentation build complete, but mkdocs not requested')
info('Documentation build complete, but zensical build not requested')
@task