Skip to content

Commit 63a5ec0

Browse files
hf-krechanlord-haffihf-kklein
authored
Rework with pydantic + svg images in documentation (#380)
### Quick summary of important changes: - switched from marshmallow + attr / attrs to [pydantic](https://pydantic-docs.helpmanual.io/) - [data validation](https://pydantic-docs.helpmanual.io/usage/validators/) and de/serialization will be processed by pydantic - serialization with `<instance>.json(by_alias=True, ensure_ascii=False)` - deserialization with `<class>.parse_raw(<json_string>)` - note: pydantic comes with some [useful types](https://pydantic-docs.helpmanual.io/usage/types/) - now schema-classes aren't neccessary anymore -> removed - json-schemas for readthedocs are created via `.schema_json()` - important note: json-schema files got renamed from `<classname>Schema.json` to `<classname>.json` - important note: Now e.g. `"2.4"` would be accepted as `Decimal` if the string is parsable (and it will be parsed!) -> a field with e.g. `Decimal` type hint will always be `Decimal`. But it can be set/initialized through e.g. a string if the string is parsable into a `Decimal`. However, the type checker would cry if you try this without "type casting" :) - auto creation of uml-diagrams and integration with sphinx - when building the docs local via `tox -e docs` or `tox`, the generated [Plantuml](https://plantuml.com/de/) (Java software)-files will be sent to [kroki.io](https://kroki.io) web service in order to generate the svg images. This means, it will be painfully slow with ICE WLAN :) - uml-diagrams are generated for all classes in `bo` and `com` on runtime via: `docs/conf.py` -> `docs/uml.py` -> creates `*.puml` files in `docs/api/uml` -> sent to kroki.io and get svg -> save to `_static` folder - `*.svg` files can be included in docstrings via an object tag, e.g. `<object data="../_static/images/bo4e/bo/Marktlokation.svg" type="image/svg+xml"></object>` - Note: The links inside the svgs are absolute. If you want svg files with other base URIs (or no base URIs, just relative paths), you just have to change `LINK_BASE_URI` in `docs/uml.py` Co-authored-by: Leon Haffmans <leon.haffmans@hochfrequenz.de> Co-authored-by: konstantin <konstantin.klein@hochfrequenz.de>
1 parent da0753e commit 63a5ec0

File tree

388 files changed

+42664
-686642
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

388 files changed

+42664
-686642
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,8 @@ Icon
162162
Network Trash Folder
163163
Temporary Items
164164
.apdisk
165+
166+
# This directory rebuilds on 'tox -e docs' therefore not needed in repo
167+
docs/api
168+
docs/plantuml.jar
169+
docs/_static/images

CONTRIBUTING.md

Lines changed: 33 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@ Feel free to open an issue if you run into any kind of problems.
1717
- pylint for linting
1818
- mypy for static type checking
1919
- pytest for unittests
20-
- Sphinx for documentation
20+
- Sphinx and Plantuml (and kroki web service) for documentation
2121
- Technical Documentation is in English; For example: "Don't use the builtin validator here because …"
2222
- But data model docstrings are in German; For example: "Ist das Ende nicht gesetzt, so ist der Zeitraum als offen zu verstehen."
2323
- Docstrings should not be trivial/useless
2424
- Bad: "Energiemenge ist eine Klasse zur Abbildung von Energiemengen." ❌ (no shit sherlock)
2525
- Good: "Eine Energiemenge ordnet einer :class:`Marktlokation` oder :class:`Messlokation`, die über die `lokations_id` referenziert werden, einen oder mehrere Energieverbräuche zu." ✔
26-
- Only sentences have a fullstop at the end.
26+
- Only sentences have a full stop at the end.
2727
- We use `snake_case` internally but serialize as `camelCase` by overriding the `data_key` property of the schema fields.
2828

2929
### How to Define an ENUM?
3030

3131
All Enums inherit from `bo4e.enum.StrEnum`.
3232
The latter is just a usual Enum with a `str` mixin (see [the official docs](https://docs.python.org/3/library/enum.html?highlight=strenum#others) for details).
33-
This allows us to precisly define how an enum value will be serialized.
33+
This allows us to precisely define how an enum value will be serialized.
3434
All enum values have UPPER_CASE names.
3535

3636
```python
@@ -56,80 +56,60 @@ class MyBo4eEnum(StrEnum):
5656

5757
### How to Define `COM`s or `BO`s
5858

59-
All COMponents inherit from `bo4e.com.COM`.
60-
All Business Objects inherit from `bo4e.bo.Geschaeftsobjekt`.
59+
All COMponents inherit from `bo4e.com.com.COM`.
60+
All Business Objects inherit from `bo4e.bo.geschaeftsobjekt.Geschaeftsobjekt`.
6161

62-
The classes are defined with the help of [`attrs`](https://www.attrs.org/).
63-
64-
For the de/serialization we use [`marshmallow`](https://marshmallow.readthedocs.io/).
65-
Each Python attr-decorated class comes with a corresponding marshmallow schema.
66-
67-
All COMpontent schemas inherit from `bo4e.com.com.COMSchema`.
68-
All Business Object schemas inherit from `bo4e.bo.Geschaeftsobjekt.GeschaeftsobjektSchema`.
62+
For data validation and de/serialization we use [`pydantic`](https://pydantic-docs.helpmanual.io/).
6963

7064
```python
7165
"""
7266
Give the module a docstring to describe what it contains
7367
"""
7468

75-
from datetime import datetime
76-
from typing import Optional
69+
from pydantic import validator
7770

78-
import attr
79-
from marshmallow import fields
71+
from datetime import datetime
72+
from typing import Optional, Dict, Any
8073

81-
from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
82-
from bo4e.com.menge import Menge, MengeSchema
74+
from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt
75+
from bo4e.com.menge import Menge
8376
from bo4e.enum.botyp import BoTyp
8477

8578

8679
# pylint: disable=too-few-public-methods
87-
@attr.s(auto_attribs=True, kw_only=True)
8880
class MeinBo(Geschaeftsobjekt):
8981
"""
9082
MeinBo ist ein ganz besonderes Business Objekt.
91-
Es kommt nur bei meinem Strom-Lieferanten zum Einsatz und beschreibt dort all die tollen Eingeschaften, die mein Verbrauchsverhalten hat.
83+
Es kommt nur bei meinem Strom-Lieferanten zum Einsatz und beschreibt dort all die tollen Eigenschaften, die mein Verbrauchsverhalten hat.
9284
"""
9385

94-
bo_typ: BoTyp = attr.ib(default=BoTyp.MEINBO)
86+
bo_typ: BoTyp = BoTyp.MEINBO
87+
9588
# required attributes
89+
9690
#: Der Lieferbeginn beschreibt den Zeitpunkt ab dem (inklusiv) mich ein Versorger seinen Kunden nennen darf
97-
lieferbeginn: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
91+
lieferbeginn: datetime
9892

99-
anzahl_freudenspruenge: int = attr.ib(validator=attr.validators.instance_of(int))
93+
anzahl_freudenspruenge: int
10094
"""
10195
Anzahl Freudensprünge beschreibt, wie oft der CEO des Stromkonzerns in die Luft gesprungen ist, als ich den Vertrag unterschrieben habe.
10296
Dieser Wert sollte im Normalfall mindestens 5 sein.
10397
"""
104-
# todo: write a validator for anzahl_freudensprunge to be >5 and raise a ValidationError otherwise
98+
# pylint:disable=unused-argument, no-self-argument
99+
@validator("anzahl_freudenspruenge")
100+
def validate_freudenspruenge(cls, anzahl_freudenspruenge: int, values: Dict[str, Any]) -> int:
101+
if anzahl_freudenspruenge <= 5:
102+
raise ValueError("You are not hyped enough. Do more than 5 joyful leaps.")
103+
return anzahl_freudenspruenge
104+
105105
# we can help you with anything you might be missing or unable to implement.
106106
# ToDo comments are just fine.
107107
# You don't need to be a perfect programmer to contribute to bo4e :)
108108

109-
#: Optionale Menge (Elektrische Energie oder Gas oder Wärme), die ich zum Lieferbeginn umsonst erhalte
110-
freimenge: Optional[Menge] = attr.ib(
111-
validator=attr.validators.optional(attr.validators.instance_of(Menge)), default=None
112-
)
113-
114-
115-
class MeinBoSchema(GeschaeftsobjektSchema):
116-
"""
117-
Schema for de-/serialization of :class:`MeinBo`
118-
"""
119-
120-
# you don't need to copy paste the docstrings to the schema
121-
122-
class_name = MeinBo
123-
# this is used so that the Python object created on deserialization/loading has the correct type
124-
125-
# required attributes
126-
lieferbeginn = fields.DateTime()
127-
# the camelCase serialization is enforced via he data_key kwarg
128-
anzahl_freudenspruenge = fields.Int(data_key="anzahlFreudenspruenge")
129-
130109
# optional attributes
131-
# when nesting you need to reference the schema here (not Menge but MengeSchema!)
132-
freimenge = fields.Nested(MengeSchema, load_default=None)
110+
111+
#: Optionale Menge (Elektrische Energie oder Gas oder Wärme), die ich zum Lieferbeginn umsonst erhalte
112+
freimenge: Optional[Menge] = None
133113

134114
```
135115

@@ -144,7 +124,7 @@ Ideally provide unittests that show:
144124
- with only the required attributes
145125
- with all attributes
146126

147-
Therefore copy one of the existing "roundtrip" tests, see f.e. `TestTarifeinschraenkung`.
127+
Therefore, copy one of the existing "roundtrip" tests, see f.e. `TestTarifeinschraenkung`.
148128

149129
## Pull Request
150130

@@ -153,16 +133,16 @@ We'd appreciate if you allowed maintainer edits.
153133

154134
## Release Workflow
155135

156-
- Check with tox all tests and lintings: `tox`
136+
- Check with tox all tests and linting: `tox`
157137
- Check with tox if the packaging works fine: `tox -e test_packaging`
158138
- Squash Merge all your changes you would like to have in the release into the main/default branch
159-
- Check that all Github Actions for tests and linting do pass (should be automatically enforced for PRs against main)
160-
- Go to the repositorys right side bar and click on "[Draft a new release](https://github.com/Hochfrequenz/BO4E-python/releases/new)"
139+
- Check that all GitHub Actions for tests and linting do pass (should be automatically enforced for PRs against main)
140+
- Go to the repositorys right sidebar and click on "[Draft a new release](https://github.com/Hochfrequenz/BO4E-python/releases/new)"
161141
- Write in the _Tag version_ field and in the _Release title_ your new version, i.e. `v0.0.6`
162-
- Add a describtion to the release (or just autogenerate the change log which will be fine for 95% of cases)
142+
- Add a description to the release (or just autogenerate the change log which will be fine for 95% of cases)
163143
- Publish the release
164144

165-
There is a Github Action which gets triggered by a release event.
145+
There is a GitHub Action which gets triggered by a release event.
166146
It will run all default tests with tox.
167147
If they pass, it will take the tag title to replace the version information in the _setup.cfg_ file.
168148
After checking the package with `twine check` it will finally upload the new package release.

docs/conf.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
# If extensions (or modules to document with autodoc) are in another directory,
1919
# add these directories to sys.path here. If the directory is relative to the
2020
# documentation root, use os.path.abspath to make it absolute, like shown here.
21+
from pathlib import Path
22+
2123
sys.path.insert(0, os.path.join(__location__, "../src"))
24+
sys.path.insert(0, os.path.join(__location__, "../docs"))
25+
from uml import PlantUMLNetwork, build_network, compile_files_kroki, write_class_umls
2226

2327
# -- Run sphinx-apidoc ------------------------------------------------------
2428
# This hack is necessary since RTD does not issue `sphinx-apidoc` before running
@@ -251,7 +255,7 @@
251255
}
252256

253257
# Grouping the document tree into LaTeX files. List of tuples
254-
# (source start file, target name, title, author, documentclass [howto/manual]).
258+
# (source start file, target name, title, author, document class [howto/manual]).
255259
latex_documents = [
256260
(
257261
"index",
@@ -288,3 +292,13 @@
288292
"sphinx": ("http://www.sphinx-doc.org/en/stable", None),
289293
"python": ("https://docs.python.org/" + python_version, None),
290294
}
295+
296+
# Create UML diagrams in plantuml format. Compile these into svg files into the _static folder.
297+
# See docs/uml.py for more details.
298+
_exec_plantuml = Path(__location__) / "plantuml.jar"
299+
_network, _namespaces_to_parse = build_network(Path(module_dir), PlantUMLNetwork)
300+
write_class_umls(_network, _namespaces_to_parse, Path(output_dir) / "uml")
301+
print("Created uml files.")
302+
303+
compile_files_kroki(Path(output_dir) / "uml", Path(output_dir).parent / "_static" / "images")
304+
print(f"Compiled uml files into svg using kroki.")

0 commit comments

Comments
 (0)