diff --git a/.gitignore b/.gitignore index ad4b890..2a433e9 100644 --- a/.gitignore +++ b/.gitignore @@ -159,8 +159,8 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - +.idea/ +.vscode/ *.json *.toml diff --git a/masterbase/S_hat.npy b/data/npy/S_hat.npy similarity index 100% rename from masterbase/S_hat.npy rename to data/npy/S_hat.npy diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/_static/css/custom.css b/docs/source/_static/css/custom.css new file mode 100644 index 0000000..80a2a04 --- /dev/null +++ b/docs/source/_static/css/custom.css @@ -0,0 +1,7 @@ +@import 'material_sphinx.css'; + +.sig.sig-object.py{ + font-feature-settings: "kern"; + font-family: "Roboto Mono", "Courier New", Courier, monospace; + background: #f8f8f8; +} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..2bc78d5 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,41 @@ +import os +import sys +sys.path.insert(0, os.path.abspath('../../masterbase')) + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'Masterbase' +copyright = '2024 MegaAntiCheat' +author = 'Jayce, Flenser, Lilith, meat🄩' +release = '0.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', + 'sphinx-pydantic' +] + +templates_path = ['_templates'] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] + + +def setup(app): + app.add_css_file('css/custom.css') diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..f007746 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. Masterbase documentation master file, created by + sphinx-quickstart on Mon May 13 12:57:00 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +MegaAntiCheat Masterbase documentation +====================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/masterbase.rst b/docs/source/masterbase.rst new file mode 100644 index 0000000..3826476 --- /dev/null +++ b/docs/source/masterbase.rst @@ -0,0 +1,61 @@ +masterbase package +================== + +Submodules +---------- + +masterbase.anomaly module +------------------------- + +.. automodule:: masterbase.anomaly + :members: + :undoc-members: + :show-inheritance: + +masterbase.app module +--------------------- + +.. automodule:: masterbase.app + :members: + :undoc-members: + :show-inheritance: + +masterbase.guards module +------------------------ + +.. automodule:: masterbase.guards + :members: + :undoc-members: + :show-inheritance: + +masterbase.lib module +--------------------- + +.. automodule:: masterbase.lib + :members: + :undoc-members: + :show-inheritance: + +masterbase.registers module +--------------------------- + +.. automodule:: masterbase.registers + :members: + :undoc-members: + :show-inheritance: + +masterbase.steam module +----------------------- + +.. automodule:: masterbase.steam + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: masterbase + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000..99519ef --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +masterbase +========== + +.. toctree:: + :maxdepth: 4 + + masterbase diff --git a/masterbase/__init__.py b/masterbase/__init__.py index fa379a1..8c3b861 100644 --- a/masterbase/__init__.py +++ b/masterbase/__init__.py @@ -1 +1,3 @@ -"""Source code for API interaction.""" +""" +Source code for API interaction. +""" diff --git a/masterbase/anomaly.py b/masterbase/anomaly.py index 15c858f..13a9f3c 100644 --- a/masterbase/anomaly.py +++ b/masterbase/anomaly.py @@ -1,4 +1,21 @@ -"""Anomaly detection for demo streams.""" +""" +Anomaly detection for demo streams. + +Classes: +- DetectionState + +Functions: +- longest_zero_run +- likelihood +- nz_markov_likelihood +- transition_freqs + +Misc variables: +- S_hat +""" + +__author__ = "Flenser, Jayce" +__version__ = "1.0.0" import os @@ -6,14 +23,18 @@ from numpy.typing import NDArray from pydantic import BaseModel, Field -S_hat: NDArray = np.load(os.path.join("masterbase", "S_hat.npy")) + +S_hat: NDArray | None = None def longest_zero_run(data: bytes) -> int: - """Get the longest zero run of data. + """ + Get the longest zero run of data. - Args: - data: Input data stream + :param data: Input data stream + :type data: bytes + :return: longest zero run + :rtype: int """ array = np.frombuffer(data, dtype=np.uint8) zero_mask = array == 0 @@ -27,31 +48,46 @@ def longest_zero_run(data: bytes) -> int: def likelihood(p: NDArray, q: NDArray) -> float: - """Determine the likelihood of an empirical frequency distribution under a prior discrete probability distribution. - - Args: - p: Discrete prior distribution - q: Observed empirical distribution, normalized + """ + Determine the likelihood of an empirical frequency distribution under a prior discrete probability distribution. + + :param p: Discrete prior distribution + :type p: NDArray + :param q: Observed empirical distribution, normalized + :type q: NDArray + :return: likelihood + :rtype: float """ return np.exp(np.sum(np.log(p + 1e-5) * q)) def nz_markov_likelihood(coocs: NDArray) -> float: - """Determine the Markov likelihood of all byte-pair transitions, between *nonzero* bytes, e.g., as determined by `transition_freqs`. + """ + Determine the Markov likelihood of all byte-pair transitions, between *nonzero* bytes, + e.g., as determined by `transition_freqs`. - Args: - coocs: cooccurrences of successive octets (transition frequencies). + :param coocs: cooccurrences of successive octets (transition frequencies). + :type coocs: NDArray + :return: likelihood + :rtype: float """ # noqa + global S_hat + if S_hat is None: + S_hat = np.load(os.path.join("data", "npy", "S_hat.npy")) + _S_hat, coocs = map(lambda a: a.reshape(-1)[1:], (S_hat, coocs)) # noqa _S_hat, coocs = map(lambda a: a / a.sum(), (_S_hat, coocs)) # noqa return float(likelihood(_S_hat, coocs)) def transition_freqs(data: bytes) -> NDArray: - """Count the cooccurrences of successive octets (transition frequencies). + """ + Count the co-occurrences of successive octets (transition frequencies). - Args: - data: bytes, the input data stream + :param data: the input data stream + :type data: bytes + :return: Array of co-occurrences + :rtype: NDArray """ array = np.frombuffer(data, dtype=np.uint8) i, j = array[:-1], array[1:] @@ -61,23 +97,27 @@ def transition_freqs(data: bytes) -> NDArray: class DetectionState(BaseModel): - """Accumulate a trace of statistics for determining the likelihood of observed data under Markov transition frequencies present in valid data. - - Args: + """ + Accumulate a trace of statistics for determining the likelihood of observed data under Markov transition + frequencies present in valid data. + + Attributes: length: The cumulative length of all inputs thus far. likelihood: The cumulative probability of observing the input data under `S_hat` so far. longest_zero_run: The longest run of consecutive zeros observed in the input data chunks so far. """ # noqa - length: int = Field(default=0) likelihood: float = Field(default=0.0) longest_zero_run: int = Field(default=0) def update(self, data: bytes) -> None: - """Update the current state trace with stats from the input data. + """ + Update the current state trace with stats from the input data. - Args: - data: bytes, input data whose likelihood to compute under the prior transition matrix to accumulate into statistics for the stream. + :param data: input data whose likelihood to compute under the prior transition matrix to accumulate + into statistics for the stream. + :type data: bytes + :return: None """ # noqa new_length = len(data) + self.length new_likelihood = ( @@ -90,5 +130,10 @@ def update(self, data: bytes) -> None: @property def anomalous(self) -> bool: - """Return if the current state is anomalous or not.""" + """ + Return if the current state is anomalous or not. + + :return: true if likelihood indicates anonymity + :rtype: bool + """ return self.likelihood <= 3e-5 or self.longest_zero_run >= 384 diff --git a/pdm.lock b/pdm.lock index 1484a28..1b395e7 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,17 @@ groups = ["default", "dev"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:df98ea4259197262441c40fafc829bf356d14f8b4b5805bf6c158a3ffda0cc00" +content_hash = "sha256:5f938310613a2fd21332a0a31e8c938a962fe15d9333aea1325502e94ebed785" + +[[package]] +name = "alabaster" +version = "0.7.16" +requires_python = ">=3.9" +summary = "A light, configurable Sphinx theme" +files = [ + {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, + {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, +] [[package]] name = "alembic" @@ -94,6 +104,16 @@ files = [ {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, ] +[[package]] +name = "babel" +version = "2.15.0" +requires_python = ">=3.8" +summary = "Internationalization utilities" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + [[package]] name = "certifi" version = "2024.2.2" @@ -182,6 +202,16 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "docutils" +version = "0.21.2" +requires_python = ">=3.9" +summary = "Docutils -- Python Documentation Utilities" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + [[package]] name = "editorconfig" version = "0.12.4" @@ -354,6 +384,16 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "imagesize" +version = "1.4.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "Getting image size from png/jpeg/jpeg2000/gif file" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -389,6 +429,16 @@ files = [ {file = "jsbeautifier-1.15.1.tar.gz", hash = "sha256:ebd733b560704c602d744eafc839db60a1ee9326e30a2a80c4adb8718adc1b24"}, ] +[[package]] +name = "jsonpointer" +version = "2.4" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +summary = "Identify specific nodes in a JSON document (RFC 6901) " +files = [ + {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, + {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, +] + [[package]] name = "litestar" version = "2.8.3" @@ -687,6 +737,18 @@ files = [ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] +[[package]] +name = "pockets" +version = "0.9.1" +summary = "A collection of helpful Python tools!" +dependencies = [ + "six>=1.5.2", +] +files = [ + {file = "pockets-0.9.1-py2.py3-none-any.whl", hash = "sha256:68597934193c08a08eb2bf6a1d85593f627c22f9b065cc727a4f03f669d96d86"}, + {file = "pockets-0.9.1.tar.gz", hash = "sha256:9320f1a3c6f7a9133fe3b571f283bcf3353cd70249025ae8d618e40e9f7e92b3"}, +] + [[package]] name = "polyfactory" version = "2.15.0" @@ -1000,6 +1062,145 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "snowballstemmer" +version = "2.2.0" +summary = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "7.3.7" +requires_python = ">=3.9" +summary = "Python documentation generator" +dependencies = [ + "Jinja2>=3.0", + "Pygments>=2.14", + "alabaster~=0.7.14", + "babel>=2.9", + "colorama>=0.4.5; sys_platform == \"win32\"", + "docutils<0.22,>=0.18.1", + "imagesize>=1.3", + "packaging>=21.0", + "requests>=2.25.0", + "snowballstemmer>=2.0", + "sphinxcontrib-applehelp", + "sphinxcontrib-devhelp", + "sphinxcontrib-htmlhelp>=2.0.0", + "sphinxcontrib-jsmath", + "sphinxcontrib-qthelp", + "sphinxcontrib-serializinghtml>=1.1.9", + "tomli>=2; python_version < \"3.11\"", +] +files = [ + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, +] + +[[package]] +name = "sphinx-jsonschema" +version = "1.19.1" +summary = "Sphinx extension to display JSON Schema" +dependencies = [ + "docutils", + "jsonpointer", + "pyyaml", + "requests", +] +files = [ + {file = "sphinx-jsonschema-1.19.1.tar.gz", hash = "sha256:b2385fe1c7acf2e759152aefed0cb17c920645b2a75c9934000c9c528e7d53c1"}, +] + +[[package]] +name = "sphinx-pydantic" +version = "0.1.1" +requires_python = ">=3.6.0" +summary = "Generate Sphinx documentation from PyDantic objects." +dependencies = [ + "pydantic", + "sphinx-jsonschema", +] +files = [ + {file = "sphinx-pydantic-0.1.1.tar.gz", hash = "sha256:a830e4f07fe88fbdfe3edecc2f52ef133cde2def7cb882a3f22780f34963b0fb"}, + {file = "sphinx_pydantic-0.1.1-py3-none-any.whl", hash = "sha256:371487ad81250d8bc5b944a2936b33c10ff88af7188d5be0ee6c4b46bb70254a"}, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.8" +requires_python = ">=3.9" +summary = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +files = [ + {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, + {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.6" +requires_python = ">=3.9" +summary = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +files = [ + {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, + {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.5" +requires_python = ">=3.9" +summary = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +files = [ + {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, + {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +requires_python = ">=3.5" +summary = "A sphinx extension which renders display math in HTML via JavaScript" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[[package]] +name = "sphinxcontrib-napoleon" +version = "0.7" +summary = "Sphinx \"napoleon\" extension." +dependencies = [ + "pockets>=0.3", + "six>=1.5.2", +] +files = [ + {file = "sphinxcontrib-napoleon-0.7.tar.gz", hash = "sha256:407382beed396e9f2d7f3043fad6afda95719204a1e1a231ac865f40abcbfcf8"}, + {file = "sphinxcontrib_napoleon-0.7-py2.py3-none-any.whl", hash = "sha256:711e41a3974bdf110a484aec4c1a556799eb0b3f3b897521a018ad7e2db13fef"}, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.7" +requires_python = ">=3.9" +summary = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +files = [ + {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, + {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.10" +requires_python = ">=3.9" +summary = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +files = [ + {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, + {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, +] + [[package]] name = "sqlalchemy" version = "2.0.30" diff --git a/pyproject.toml b/pyproject.toml index 19d6fc4..380cb1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,9 @@ dev = [ "pytest>=7.4.3", "types-toml>=0.10.8.20240310", "types-requests>=2.31.0.20240406", + "sphinx>=7.3.7", + "sphinxcontrib-napoleon>=0.7", + "sphinx-pydantic>=0.1.1", ] [build-system] requires = ["pdm-backend"]