├── fabricius ├── py.typed ├── app │ ├── main.py │ ├── ui.py │ └── signals.py ├── __init__.py ├── renderers │ ├── mustache.py │ ├── __init__.py │ ├── python_format.py │ ├── utils.py │ ├── jinja_renderer.py │ └── python_string_template.py ├── readers │ ├── README.md │ └── cookiecutter │ │ ├── exceptions.py │ │ ├── config.py │ │ ├── extensions.py │ │ ├── hooks.py │ │ └── setup.py ├── globals.py ├── models │ ├── renderer.py │ ├── signal.py │ ├── template.py │ └── file.py ├── types.py ├── exceptions.py └── utils.py ├── tests ├── __init__.py ├── files │ └── templates │ │ ├── string_template.txt │ │ ├── python_template.txt │ │ ├── jinja_template.jinja │ │ └── mustache_template.mustache ├── test_meta.py ├── test_renderer.py └── test_file.py ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── docs ├── source │ ├── _static │ │ ├── logo-dark.png │ │ ├── logo-white.png │ │ └── furo_custom.css │ ├── guides │ │ ├── guide_rendering.rst │ │ ├── guide_create_forge_file.rst │ │ └── guide_cookiecutter_templates.rst │ ├── api │ │ ├── exceptions.rst │ │ ├── types.rst │ │ ├── models.rst │ │ ├── renderers.rst │ │ └── signals.rst │ ├── index.rst │ └── conf.py ├── Makefile └── make.bat ├── .editorconfig ├── .readthedocs.yml ├── tox.ini ├── .vscode ├── launch.json └── tasks.json ├── .sourcery.yaml ├── LICENSE ├── .pre-commit-config.yaml ├── pyproject.toml ├── README.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── requirements ├── requirements.txt ├── requirements-docs.txt └── requirements-dev.txt └── CONTRIBUTING.md /fabricius/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: predeactor 2 | ko_fi: predeactor 3 | -------------------------------------------------------------------------------- /tests/files/templates/string_template.txt: -------------------------------------------------------------------------------- 1 | Hello $name! 2 | 3 | I'm $name and I'm $missing (Miss!). 4 | -------------------------------------------------------------------------------- /tests/files/templates/python_template.txt: -------------------------------------------------------------------------------- 1 | Hello {name}! 2 | 3 | I'm {name} and I'm {missing} (Miss!). 4 | -------------------------------------------------------------------------------- /docs/source/_static/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebylydia/Fabricius/HEAD/docs/source/_static/logo-dark.png -------------------------------------------------------------------------------- /tests/files/templates/jinja_template.jinja: -------------------------------------------------------------------------------- 1 | Hello {{ name }}! 2 | 3 | I'm {{ name }} and I'm {{ missing }} (Miss!). 4 | -------------------------------------------------------------------------------- /docs/source/_static/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebylydia/Fabricius/HEAD/docs/source/_static/logo-white.png -------------------------------------------------------------------------------- /fabricius/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class App: 5 | log: logging.Logger = logging.getLogger("fabricius") 6 | -------------------------------------------------------------------------------- /tests/files/templates/mustache_template.mustache: -------------------------------------------------------------------------------- 1 | Hello {{ name }}! 2 | 3 | I'm {{ name }} and I'm {{ missing }} (Miss!). 4 | -------------------------------------------------------------------------------- /fabricius/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import distribution as __dist 2 | 3 | __version__ = __dist("fabricius").version 4 | __author__ = __dist("fabricius").metadata["Author"] 5 | -------------------------------------------------------------------------------- /docs/source/guides/guide_rendering.rst: -------------------------------------------------------------------------------- 1 | Guide: Rendering files & templates 2 | ================================== 3 | 4 | Oh hey, nice finding! Sadly, this is a work in progress, come check this out later :) 5 | -------------------------------------------------------------------------------- /fabricius/renderers/mustache.py: -------------------------------------------------------------------------------- 1 | import chevron 2 | 3 | from fabricius.models.renderer import Renderer 4 | 5 | 6 | class ChevronRenderer(Renderer): 7 | name = "Chevron (Moustache)" 8 | 9 | def render(self, content: str) -> str: 10 | return chevron.render(content, self.data) 11 | -------------------------------------------------------------------------------- /docs/source/api/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | 4 | Inside all of our logic behind it, we have created supplementary exceptions to explain what could have gone wrong either in your template, or potentially what could have badly happened. 5 | 6 | .. automodule:: fabricius.exceptions 7 | :members: 8 | -------------------------------------------------------------------------------- /fabricius/renderers/__init__.py: -------------------------------------------------------------------------------- 1 | from .jinja_renderer import JinjaRenderer as JinjaRenderer 2 | from .mustache import ChevronRenderer as ChevronRenderer 3 | from .python_format import PythonFormatRenderer as PythonFormatRenderer 4 | from .python_string_template import StringTemplateRenderer as StringTemplateRenderer 5 | -------------------------------------------------------------------------------- /fabricius/renderers/python_format.py: -------------------------------------------------------------------------------- 1 | from fabricius.models.renderer import Renderer 2 | 3 | from .utils import DictAllowMiss 4 | 5 | 6 | class PythonFormatRenderer(Renderer): 7 | name = "Python str.format" 8 | 9 | def render(self, content: str) -> str: 10 | return content.format_map(DictAllowMiss(self.data)) 11 | -------------------------------------------------------------------------------- /fabricius/readers/README.md: -------------------------------------------------------------------------------- 1 | # Readers 2 | 3 | Readers are what permits Fabricius to make advantages of 3rd-party project templating engines using Fabricius's objects. 4 | Each readers define a "setup" functions that will be ran against certain defined parameters. 5 | 6 | In the end, the "setup" function must return a "Template" object from Fabricius. 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.{json,yaml,yml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/source/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3" 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | - requirements: requirements/requirements-docs.txt 16 | 17 | formats: 18 | - pdf 19 | - epub 20 | - htmlzip 21 | -------------------------------------------------------------------------------- /docs/source/api/types.rst: -------------------------------------------------------------------------------- 1 | Types 2 | ===== 3 | 4 | This page is fairly short, but deserves to be shown. 5 | 6 | This explains you the specific types available in Fabricius. 7 | They are widely used inside Fabricius in order to simply how the docs is rendered and to facilitate understanding of the library. 8 | 9 | 10 | Fabricius types 11 | 12 | .. automodule:: fabricius.types 13 | :members: 14 | -------------------------------------------------------------------------------- /fabricius/renderers/utils.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | 4 | class DictAllowMiss(typing.Dict[str, typing.Any]): 5 | """ 6 | A subclass of Dict to return an empty string in case of missing key, instead of raising an 7 | error, so that it can play nice with renderer. 8 | 9 | :meta private: 10 | """ 11 | 12 | def __missing__(self, _: str) -> typing.Literal[""]: 13 | return "" 14 | -------------------------------------------------------------------------------- /fabricius/renderers/jinja_renderer.py: -------------------------------------------------------------------------------- 1 | from jinja2 import BaseLoader, Environment 2 | 3 | from fabricius.models.renderer import Renderer 4 | 5 | 6 | class JinjaRenderer(Renderer): 7 | name = "Jinja Template" 8 | 9 | environment: Environment = Environment(loader=BaseLoader()) 10 | 11 | def render(self, content: str) -> str: 12 | return self.environment.from_string(content).render(**self.data) 13 | -------------------------------------------------------------------------------- /fabricius/globals.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from threading import local 3 | 4 | if typing.TYPE_CHECKING: 5 | from .app.main import App 6 | 7 | 8 | _local = local() 9 | 10 | 11 | def get_app() -> "App": 12 | try: 13 | return typing.cast("App", _local.app) 14 | except AttributeError as e: 15 | raise RuntimeError("App has not been set.") from e 16 | 17 | 18 | def set_app(app: "App") -> None: 19 | _local.app = app 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py310,py311 8 | skip_missing_interpreters = true 9 | skipsdist = true 10 | 11 | [gh-actions] 12 | python = 13 | 3.10: py310 14 | 3.11: py311 15 | 16 | [testenv] 17 | allowlist_externals = poetry 18 | commands = 19 | poetry install -v --only main 20 | poetry run python -m unittest 21 | -------------------------------------------------------------------------------- /fabricius/readers/cookiecutter/exceptions.py: -------------------------------------------------------------------------------- 1 | from fabricius.exceptions import FabriciusError 2 | 3 | 4 | class FailedHookError(FabriciusError): 5 | """ 6 | Raised when a hook run fails. 7 | """ 8 | 9 | exit_code: int | None 10 | 11 | def __init__( 12 | self, hook: str, reason: str | None = None, *, exit_code: int | None = None 13 | ) -> None: 14 | super().__init__( 15 | f"Hook {hook} failed to run: {reason}" 16 | if reason 17 | else f"Hook {hook} has failed to run. No reason provided." 18 | ) 19 | self.exit_code = exit_code 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch tests", 6 | "type": "python", 7 | "module": "unittest", 8 | "args": [ 9 | "--failfast" 10 | ], 11 | "request": "launch", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | }, 15 | { 16 | "type": "firefox", 17 | "request": "launch", 18 | "reAttach": true, 19 | "preLaunchTask": "Build docs", 20 | "name": "Launch docs", 21 | "file": "${workspaceFolder}/docs/build/html/index.html" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /docs/source/api/models.rst: -------------------------------------------------------------------------------- 1 | Models 2 | ====== 3 | 4 | Models represents the objects used inside Fabricius, for example, the :py:class:`File ` model is an object that represent a file we're about to create. 5 | 6 | In this page, you're able to visit the available methods in our models and freely uses them. 7 | 8 | 9 | .. autoclass:: fabricius.models.file.File 10 | :members: 11 | 12 | 13 | .. autoclass:: fabricius.models.signal.Signal 14 | :members: 15 | 16 | 17 | .. autoclass:: fabricius.models.renderer.Renderer 18 | :members: 19 | 20 | 21 | .. autoclass:: fabricius.models.template.Template 22 | :members: 23 | :undoc-members: 24 | -------------------------------------------------------------------------------- /tests/test_meta.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from fabricius import __version__ as fabricius_version 4 | from fabricius.utils import calculate_text_color 5 | 6 | 7 | def test_version_is_semantic(): 8 | # I hope the guy who made this regex rests in peace. 9 | regex = re.match( 10 | r"^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$", 11 | fabricius_version, 12 | ) 13 | print(regex) 14 | assert regex is not None, "Your version is not semantic." 15 | 16 | 17 | def test_color_calculation(): 18 | assert calculate_text_color("black") == "bright_white" 19 | assert calculate_text_color("bright_white") == "black" 20 | -------------------------------------------------------------------------------- /.sourcery.yaml: -------------------------------------------------------------------------------- 1 | version: '1' # The schema version of this config file 2 | 3 | ignore: 4 | - .git 5 | - venv 6 | - .venv 7 | - env 8 | - .env 9 | - .tox 10 | 11 | rule_settings: 12 | enable: 13 | - default 14 | disable: [] 15 | rule_types: 16 | - refactoring 17 | - suggestion 18 | - comment 19 | python_version: '3.10' 20 | 21 | rules: # https://docs.sourcery.ai/custom_rules/reference/ 22 | - id: only-allow-import-typing 23 | description: Import typing using import statement, not from statement. 24 | pattern: from typing import ... 25 | language: python 26 | replacement: import typing 27 | paths: 28 | include: 29 | - './fabricius' 30 | tests: 31 | - match: 'from typing import Any' 32 | - no-match: 'import typing' 33 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Build docs", 6 | "type": "shell", 7 | "command": "poetry run make html", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "options": { 13 | "cwd": "${workspaceFolder}/docs" 14 | } 15 | }, 16 | { 17 | "label": "Autobuild API docs", 18 | "type": "shell", 19 | "command": "poetry", 20 | "args": ["run", "sphinx-apidoc", "${cwd}/fabricius", "-o", "${cwd}/docs/source/api", "--force"], 21 | "group": { 22 | "kind": "build" 23 | }, 24 | "options": { 25 | "cwd": "${workspaceFolder}" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | livehtml: 15 | sphinx-autobuild --watch . --watch ../fabricius "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 16 | 17 | .PHONY: help Makefile 18 | 19 | # Catch-all target: route all unknown targets to Sphinx using the new 20 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 21 | %: Makefile 22 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 23 | -------------------------------------------------------------------------------- /fabricius/renderers/python_string_template.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from fabricius.models.renderer import Renderer 4 | from fabricius.types import Data 5 | 6 | from .utils import DictAllowMiss 7 | 8 | 9 | class StringTemplateRenderer(Renderer): 10 | name = "Python string.Template" 11 | 12 | safe: bool 13 | """ 14 | Indicate if the renderer should use 15 | :py:meth:`string.Template.safe_substitute` or 16 | :py:meth:`string.Template.substitute` 17 | """ 18 | 19 | def __init__(self, data: Data, *, safe: bool = True) -> None: 20 | self.safe = safe 21 | super().__init__(data) 22 | 23 | def render(self, content: str) -> str: 24 | if self.safe: 25 | return string.Template(content).safe_substitute(DictAllowMiss(self.data)) 26 | else: 27 | return string.Template(content).substitute(self.data) 28 | -------------------------------------------------------------------------------- /docs/source/guides/guide_create_forge_file.rst: -------------------------------------------------------------------------------- 1 | Guide: Create your Forge file 2 | ============================= 3 | 4 | .. important:: 5 | 6 | The forge file is still not supported in Fabricius. This guide will just show you what to expect out of it. 7 | 8 | The Forge file is a file created in either your repo or in a template that allows you to define what Fabricius will do. Basically, it's a configuration file, like the `cookiecutter.json` for CookieCutter. 9 | The difference with other tools is that we use a Python file to allows you more customization, with this approach, you can not only define how you want to create your template(s), but also: 10 | 11 | 1. Run some code you've made yourself instead of launching Fabricius (It's up to you to create files! Perfect for use with the :py:class:`Generator `!) 12 | 2. Add plugins when running Fabricius 13 | 3. Have a fully type-hinted/type-safe config file 14 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022-present Julien Mauroy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /docs/source/_static/furo_custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Ok... So what did we do here? 3 | * Nothing much honestly, we "expanded" Furo. I thought it was really a shame it wasn't using 4 | * the full place of the screen (Most especially for API docs), so the sidebar will now be mostly 5 | * present on the side and not take an enormeous place for nothing, and the docs's content will 6 | * take 80% of the screen now (Instead of a ridiculous 40% IMO). 7 | */ 8 | 9 | .sidebar-drawer { 10 | width: auto; 11 | } 12 | 13 | .content { 14 | width: auto; 15 | } 16 | 17 | .toc-drawer { 18 | display: flex; 19 | flex: none; 20 | } 21 | 22 | @media (max-width: 82em) { 23 | .toc-drawer { 24 | border-left: initial; 25 | height: initial; 26 | position: initial; 27 | right: initial; 28 | top: initial; 29 | } 30 | } 31 | 32 | @media (max-width: 52em) { 33 | .toc-drawer { 34 | border-left: 1px solid var(--color-background-muted); 35 | height: 100vh; 36 | position: fixed; 37 | right: -15em; 38 | top: 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Fabricius Tests 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | push: 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | python-version: ['3.10', '3.11'] 12 | 13 | runs-on: ubuntu-latest 14 | name: Tests - py${{ matrix.python-version }} 15 | 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Load cached Poetry installation 21 | uses: actions/cache@v3 22 | with: 23 | path: ~/.local 24 | key: poetry-0 # To reset cache, increment 25 | 26 | - name: Install Poetry 27 | if: steps.cached-poetry.outputs.cache-hit != 'true' 28 | uses: snok/install-poetry@v1 29 | 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | cache: 'poetry' 35 | 36 | - name: Install package 37 | run: poetry install --with dev,actions 38 | 39 | - name: Run Tox 40 | run: poetry run tox 41 | -------------------------------------------------------------------------------- /fabricius/readers/cookiecutter/config.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from copy import deepcopy 3 | from pathlib import Path 4 | 5 | import yaml 6 | 7 | 8 | class Config(typing.TypedDict): 9 | default_context: dict[typing.Any, typing.Any] 10 | 11 | 12 | DEFAULT_CONFIG: Config = { 13 | "default_context": {}, 14 | } 15 | 16 | 17 | def read_config_file() -> Config: 18 | try: 19 | config = Path("~/.cookiecutterrc").expanduser().read_text() 20 | return yaml.safe_load(config) 21 | except FileNotFoundError: 22 | return DEFAULT_CONFIG 23 | 24 | 25 | def deep_merge( 26 | base: dict[typing.Any, typing.Any], update_with: dict[typing.Any, typing.Any] 27 | ) -> dict[typing.Any, typing.Any]: 28 | data = deepcopy(base) 29 | 30 | for key, value in update_with.items(): 31 | if isinstance(value, dict): 32 | data[key] = deep_merge(base.get(key, {}), value) 33 | else: 34 | data[key] = value 35 | 36 | return data 37 | 38 | 39 | def get_config() -> Config: 40 | conf_data = read_config_file() 41 | 42 | data: Config = deep_merge(DEFAULT_CONFIG, conf_data) # wtf? 43 | 44 | return Config(default_context=data["default_context"]) 45 | -------------------------------------------------------------------------------- /fabricius/models/renderer.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import typing 3 | 4 | from fabricius.types import Data 5 | 6 | 7 | class Renderer(abc.ABC): 8 | """ 9 | The Renderer is what translate and generates the output of templates. Core of the work. 10 | 11 | You must subclass this class and override the :py:meth:`render` method, if possible, also add 12 | a name. 13 | """ 14 | 15 | name: typing.ClassVar[str | None] = None 16 | """ 17 | The name of the renderer, not necessary, but suggested to add. 18 | """ 19 | 20 | data: Data 21 | """ 22 | A dictionary that contains data passed by the users to pass inside the template. 23 | """ 24 | 25 | def __init__(self, data: Data) -> None: 26 | self.data = data 27 | 28 | @abc.abstractmethod 29 | def render(self, content: str) -> str: 30 | """ 31 | This method will process a given string, the template input and return the processed 32 | template as a string too. 33 | 34 | Parameters 35 | ---------- 36 | content : :py:class:`str` 37 | The template 38 | 39 | Returns 40 | ------- 41 | :py:class:`str` : 42 | The result of the processed template. 43 | """ 44 | raise NotImplementedError() 45 | -------------------------------------------------------------------------------- /fabricius/models/signal.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import typing 3 | 4 | _F = typing.ParamSpec("_F") 5 | 6 | 7 | class Signal(typing.Generic[_F]): 8 | """ 9 | The Listener is the base class used to create listeners of events. 10 | """ 11 | 12 | listeners: list[typing.Callable[_F, typing.Any]] 13 | """ 14 | The list of listeners that are subscribed to this signal. 15 | """ 16 | 17 | def __init__(self, *, func_hint: typing.Callable[_F, typing.Any] | None = None) -> None: 18 | self.listeners = [] 19 | 20 | def connect(self, listener: typing.Callable[_F, typing.Any]) -> None: 21 | """ 22 | Connect a listener to this signal. 23 | """ 24 | if listener not in self.listeners: 25 | self.listeners.append(listener) 26 | 27 | def disconnect(self, listener: typing.Callable[_F, typing.Any]) -> None: 28 | """ 29 | Disconnect a listener to this signal. 30 | """ 31 | if listener in self.listeners: 32 | self.listeners.remove(listener) 33 | 34 | def send(self, *args: _F.args, **kwargs: _F.kwargs) -> list[typing.Any]: 35 | """ 36 | Sends the signal to all subscribed listeners. 37 | """ 38 | results: list[typing.Any] = [] 39 | for listener in self.listeners: 40 | with contextlib.suppress(NotImplementedError): 41 | result = listener(*args, **kwargs) 42 | results.append(result) 43 | return results 44 | -------------------------------------------------------------------------------- /docs/source/api/renderers.rst: -------------------------------------------------------------------------------- 1 | Renderers 2 | ========= 3 | 4 | The "Renderer" is a class that is created in order to generate the content of a template. 5 | 6 | Fabricius ships many by default, you can use them, or create your own if you feel the need to. 7 | 8 | .. code-block:: py 9 | 10 | from fabricius.renderers import Renderer 11 | 12 | class MyRenderer(Renderer): 13 | 14 | # You must implement the "render" method, this will be called by Fabricius. 15 | def render(self, content: str): 16 | # Inside of the Renderer class, the "data" property is available. 17 | # This is where the data is stored. 18 | 19 | final_content = render_content(content=content, data=self.data) 20 | 21 | return final_content 22 | 23 | renderer = MyRenderer({"name": "John"}) 24 | final_content = renderer.render("Hello {{ name }}") 25 | print(final_content) 26 | # Hello John 27 | 28 | The following is the list of the available renderer packaged with Fabricius. It contains Python's str.format, string template, Mustache & Jinja. 29 | 30 | .. hint:: 31 | 32 | If you're using the :py:class:`File ` object, you can use methods :py:meth:`File.use_jinja() ` to set the renderer to one of Fabricius's available. 33 | To use your own Renderer, use :py:meth:`File.with_renderer() `. 34 | 35 | .. automodule:: fabricius.renderers 36 | :members: 37 | :imported-members: 38 | -------------------------------------------------------------------------------- /fabricius/types.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import typing 3 | 4 | if typing.TYPE_CHECKING: 5 | import os 6 | 7 | Data: typing.TypeAlias = "dict[str, typing.Any]" 8 | """ 9 | Data is an alias that represents a dictionary of which every keys are string and their values 10 | of any types. 11 | """ 12 | 13 | PathStrOrPath: typing.TypeAlias = "str | os.PathLike[str] | pathlib.Path" 14 | """ 15 | PathStrOrPath represents a path as a :py:class:`str` or a :py:class:`pathlib.Path` object. 16 | Used to help users either have an easy way to give their path. 17 | """ 18 | 19 | FILE_STATE = typing.Literal["pending", "persisted"] 20 | 21 | 22 | class FileCommitResult(typing.TypedDict): 23 | """ 24 | A FileCommitResult is returned when a file was successfully saved. 25 | It returns its information after its creation. 26 | """ 27 | 28 | name: str 29 | """ 30 | The name of the file. 31 | """ 32 | 33 | state: FILE_STATE 34 | """ 35 | The state of the file. Should always be "persisted". 36 | """ 37 | 38 | destination: pathlib.Path 39 | """ 40 | Where the file is located/has been saved. 41 | """ 42 | 43 | data: Data 44 | """ 45 | The data that has been passed during rendering. 46 | """ 47 | 48 | template_content: str 49 | """ 50 | The original content of the template. 51 | """ 52 | 53 | content: str 54 | """ 55 | The resulting content of the saved file. 56 | """ 57 | 58 | fake: bool 59 | """ 60 | If the file was faked. 61 | If faked, the file has not been saved to the disk. 62 | """ 63 | -------------------------------------------------------------------------------- /fabricius/app/ui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import contextmanager 3 | from typing import Any, Generator 4 | 5 | from rich.progress import ( 6 | BarColumn, 7 | Progress, 8 | TaskID, 9 | TaskProgressColumn, 10 | TextColumn, 11 | TimeRemainingColumn, 12 | ) 13 | 14 | from fabricius.app.main import logging 15 | from fabricius.app.signals import after_file_commit 16 | from fabricius.models.file import File 17 | from fabricius.types import FileCommitResult 18 | 19 | _log = logging.getLogger(__name__) 20 | 21 | 22 | class TemplateProgressBar: 23 | total_files: int 24 | """ 25 | The total files to process. 26 | Used to be set as the maximum in the progress bar. 27 | """ 28 | 29 | progress: Progress 30 | 31 | task: TaskID | None 32 | 33 | def __init__(self, total_files: int) -> None: 34 | self.total_files = total_files 35 | self.progress = Progress( 36 | TextColumn("[progress.description]{task.description}"), 37 | BarColumn(), 38 | TaskProgressColumn(), 39 | TimeRemainingColumn(), 40 | transient=True, 41 | ) 42 | self.task = None 43 | 44 | @contextmanager 45 | def begin(self, first_message: str) -> Generator[Progress, Any, None]: 46 | try: 47 | after_file_commit.connect(self._increase) 48 | self.task = self.progress.add_task(first_message, total=self.total_files) 49 | with self.progress as progress: 50 | yield progress 51 | finally: 52 | after_file_commit.disconnect(self._increase) 53 | self.progress.stop() 54 | 55 | def _increase(self, file: File, result: FileCommitResult) -> None: 56 | if self.task is None: 57 | _log.warning("Progress or task not detected. Ignoring.") 58 | return 59 | self.progress.update(self.task, advance=1, description=file.name) 60 | -------------------------------------------------------------------------------- /fabricius/app/signals.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from fabricius.models.signal import Signal 4 | 5 | if typing.TYPE_CHECKING: 6 | from fabricius.models.file import File, FileCommitResult 7 | from fabricius.models.template import Template 8 | 9 | def before_file_commit_hint(file: File): 10 | ... 11 | 12 | def on_file_commit_fail_hint(file: File): 13 | ... 14 | 15 | def after_file_commit_hint(file: File, result: FileCommitResult): 16 | ... 17 | 18 | def before_template_commit_hint(template: Template[typing.Any]): 19 | ... 20 | 21 | def after_template_commit_hint( 22 | template: Template[typing.Any], files_commits: list[FileCommitResult] 23 | ): 24 | ... 25 | 26 | else: 27 | before_file_commit_hint = None 28 | on_file_commit_fail_hint = None 29 | after_file_commit_hint = None 30 | before_template_commit_hint = None 31 | after_template_commit_hint = None 32 | 33 | 34 | before_file_commit = Signal(func_hint=before_file_commit_hint) 35 | """ 36 | A Signal called when a :py:obj:`File ` is about to commit a file. 37 | """ 38 | 39 | on_file_commit_fail = Signal(func_hint=on_file_commit_fail_hint) 40 | """ 41 | A Signal called when a :py:obj:`File ` had an exception occurring 42 | when committing a file. 43 | """ 44 | 45 | after_file_commit = Signal(func_hint=after_file_commit_hint) 46 | """ 47 | A Signal called when a :py:obj:`File ` has committed a file. 48 | """ 49 | 50 | before_template_commit = Signal(func_hint=before_template_commit_hint) 51 | """ 52 | A Signal called when a :py:obj:`Template ` is about to commit 53 | files. 54 | """ 55 | 56 | after_template_commit = Signal(func_hint=after_template_commit_hint) 57 | """ 58 | A Signal called when a :py:obj:`Template ` has committed all 59 | the files. 60 | """ 61 | -------------------------------------------------------------------------------- /tests/test_renderer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fabricius.models.renderer import Renderer 4 | from fabricius.renderers import ( 5 | ChevronRenderer, 6 | PythonFormatRenderer, 7 | StringTemplateRenderer, 8 | ) 9 | 10 | 11 | @pytest.fixture 12 | def dumb_renderer() -> "type[Renderer]": 13 | class MyRenderer(Renderer): 14 | def render(self, content: str) -> str: 15 | return content.format(self.data) 16 | 17 | return MyRenderer 18 | 19 | 20 | def test_renderer_data(dumb_renderer: type[Renderer]): 21 | """ 22 | Test Renderer's data availability. 23 | """ 24 | assert isinstance(dumb_renderer({}).data, dict) 25 | 26 | 27 | def test_python_format_renderer(): 28 | """ 29 | Test Python Format renderer. 30 | """ 31 | renderer = PythonFormatRenderer({"name": "Python Format"}) 32 | result = renderer.render("I am {name}") 33 | 34 | assert result == "I am Python Format" 35 | 36 | 37 | def test_string_template_renderer(): 38 | """ 39 | Test String Template renderer. 40 | """ 41 | renderer = StringTemplateRenderer({"name": "String Template"}) 42 | result = renderer.render("I am $name") 43 | assert result == "I am String Template" 44 | 45 | renderer = StringTemplateRenderer({"name_renderer": "String Template"}, safe=False) 46 | with pytest.raises(KeyError): 47 | renderer.render("I am $name") 48 | 49 | 50 | def test_chevron_renderer(): 51 | """ 52 | Test Chevron (Mustache) renderer. 53 | """ 54 | renderer = ChevronRenderer( 55 | { 56 | "name": "Chevron", 57 | "value": 10000, 58 | "taxed_value": 10000 - (10000 * 0.4), 59 | "in_ca": True, 60 | } 61 | ) 62 | result = renderer.render( 63 | "Hello {{name}}\nYou have just won {{value}} dollars!\n{{#in_ca}}\nWell, {{taxed_value}} dollars, " 64 | "after taxes.{{/in_ca}}" 65 | ) 66 | 67 | assert ( 68 | result 69 | == "Hello Chevron\nYou have just won 10000 dollars!\nWell, 6000.0 dollars, after taxes." 70 | ) 71 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.3.0 6 | hooks: 7 | # Add an empty line to all file 8 | - id: end-of-file-fixer 9 | - id: mixed-line-ending 10 | exclude: ^.git/ 11 | args: 12 | - "--fix=crlf" 13 | 14 | # Trims trailing whitespace 15 | - id: trailing-whitespace 16 | 17 | # Ensure that links to code on GitHub use the permalinks 18 | - id: check-vcs-permalinks 19 | 20 | # Syntax validation 21 | - id: check-ast 22 | - id: check-json 23 | - id: check-toml 24 | - id: check-yaml 25 | 26 | # JSON auto-formatter 27 | - id: pretty-format-json 28 | args: 29 | - "--autofix" 30 | - "--indent=2" 31 | - "--no-sort-keys" 32 | 33 | # Checks for git-related issues 34 | - id: check-case-conflict 35 | - id: check-merge-conflict 36 | 37 | - repo: https://github.com/psf/black 38 | rev: 23.3.0 39 | hooks: 40 | - id: black 41 | 42 | - repo: https://github.com/pycqa/isort 43 | rev: 5.12.0 44 | hooks: 45 | - id: isort 46 | 47 | - repo: https://github.com/compilerla/conventional-pre-commit 48 | rev: v1.3.0 49 | hooks: 50 | - id: conventional-pre-commit 51 | name: Check commit msg 52 | stages: [commit-msg] 53 | 54 | - repo: https://github.com/python-poetry/poetry 55 | rev: '1.5.0' # add version here 56 | hooks: 57 | - id: poetry-check 58 | - id: poetry-lock 59 | - id: poetry-export 60 | name: "export requirements" 61 | args: ["-f", "requirements.txt", "-o", "requirements/requirements.txt"] 62 | - id: poetry-export 63 | name: "export dev requirements" 64 | args: ["-f", "requirements.txt", "--with", "dev", "-o", "requirements/requirements-dev.txt"] 65 | - id: poetry-export 66 | name: "export docs requirements" 67 | args: ["-f", "requirements.txt", "--with", "docs", "-o", "requirements/requirements-docs.txt"] 68 | -------------------------------------------------------------------------------- /docs/source/api/signals.rst: -------------------------------------------------------------------------------- 1 | Signals 2 | ======= 3 | 4 | Signals are `observers `_. 5 | They permit you to run code when a specific action is happening. 6 | 7 | There is a lot of signal that Fabricius raises so you can subscribe to any thing you'd like. 8 | For example, before committing a file, you can add a suffix to its name before it get committed. 9 | 10 | .. code-block:: py 11 | 12 | from fabricius.app.signals import before_file_commit 13 | from fabricius.models.file import File 14 | 15 | on_file_commit(file: File): 16 | file.name = f"{file.name}.template" 17 | 18 | before_file_commit.connect(on_file_commit) 19 | 20 | Here is a list of the available signals raised in Fabricius. 21 | 22 | .. automodule:: fabricius.app.signals 23 | :members: 24 | 25 | 26 | Create your own signals 27 | ----------------------- 28 | 29 | You can create your own signal by creating a :py:class:`fabricius.models.signal.Signal` object and letting it available in your project. 30 | 31 | .. code-block:: py 32 | 33 | from fabricius.models.signal import Signal 34 | 35 | my_signal = Signal() 36 | 37 | While this is totally OK to go like this, you can also optionally type the :py:meth:`.send() `/:py:meth:`.connect() ` methods by providing a function. 38 | Fabricius will extract the function's signature and use it to transfer the arguments into the signal's methods. 39 | 40 | .. code-block:: py 41 | 42 | from fabricius.models.file import File 43 | from fabricius.models.signal import Signal 44 | 45 | def my_signal_hint(file: File): 46 | ... 47 | 48 | my_signal = Signal(func_hint=my_signal_hint) 49 | 50 | my_signal.send(File("test.py")) # This is OK 51 | my_signal.send() # This is not! Your type checker will complain! 52 | 53 | # Good 54 | def signal_receiver(file: File): 55 | ... 56 | my_signal.connect(signal_receiver) 57 | 58 | # Bad 59 | def signal_receiver(receiving_file: File): 60 | ... 61 | my_signal.connect(signal_receiver) 62 | 63 | # This will raise a type error if the function's signature is altered 64 | # (New, removed, renamed arguments, etc...) 65 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Fabricius: The Documentation 2 | ============================ 3 | 4 | .. module:: fabricius 5 | 6 | **Fabricius: Python templates renderer** 7 | 8 | Fabricius is a tool that allows you to render files template & projects template. 9 | 10 | Key features of Fabricius: 11 | 12 | - (Will) Ship with its own project templating solution 13 | - Supports CookieCutter templates 14 | - Extendable with `observers `_ (AKA signals) 15 | - User-friendly API 16 | 17 | Installation 18 | ------------ 19 | 20 | The primary requirement of Fabricius is `Python `_. 21 | It must be a version equal to or greater to Python ``3.10``. 22 | 23 | You can install Fabricius using ``pip``, the Python's package manager (It comes bundled with Python). Install Fabricius using the following command: 24 | 25 | .. code-block:: 26 | 27 | pip install Fabricius 28 | 29 | .. note:: 30 | 31 | Typically, Fabricius should be installed globally on your system (As you shouldn't need it in a specific project, it's a tool). 32 | As such, Windows might tell you to add the ``--user`` option, if so, try doing ``pip install fabricius --user``! 33 | 34 | Guides 35 | ------ 36 | 37 | .. important:: 38 | Guides are not ready yet! 39 | 40 | Fabricius need more time to get ready! While we're working on the documentation too, Fabricius is **not ready**! 41 | Guides (for now) are here to show you how Fabricius can work and how you should expect things to work out. 42 | 43 | .. toctree:: 44 | :caption: Guides 45 | :maxdepth: 1 46 | 47 | guides/guide_create_forge_file 48 | guides/guide_rendering 49 | guides/guide_cookiecutter_templates 50 | 51 | 52 | API 53 | --- 54 | 55 | .. topic:: Careful here, commander! 56 | 57 | This section is reserved for the peoples that are interested to use more complex tools in order to better understand how Fabricius works behind the scene & use it themselves. 58 | 59 | If you believe your use case is easy to tackle down, then you probably don't need to dig into the Fabricius's API. 60 | 61 | 62 | .. toctree:: 63 | :caption: API 64 | :maxdepth: 2 65 | 66 | api/models 67 | api/renderers 68 | api/types 69 | api/signals 70 | api/exceptions 71 | -------------------------------------------------------------------------------- /fabricius/readers/cookiecutter/extensions.py: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/cookiecutter/cookiecutter/blob/cf81d63bf3d82e1739db73bcbed6f1012890e33e/cookiecutter/extensions.py 2 | 3 | import json 4 | import string 5 | import typing 6 | import uuid 7 | from secrets import choice 8 | 9 | from jinja2 import Environment 10 | from jinja2.ext import Extension 11 | from slugify import slugify as pyslugify # type: ignore 12 | 13 | 14 | class JsonifyExtension(Extension): 15 | """Jinja2 extension to convert a Python object to JSON.""" 16 | 17 | def __init__(self, environment: Environment) -> None: 18 | """Initialize the extension with the given environment.""" 19 | super().__init__(environment) 20 | 21 | def jsonify(obj: object) -> str: 22 | return json.dumps(obj, sort_keys=True, indent=4) 23 | 24 | environment.filters["jsonify"] = jsonify # type: ignore 25 | 26 | 27 | class RandomStringExtension(Extension): 28 | """Jinja2 extension to create a random string.""" 29 | 30 | def __init__(self, environment: Environment) -> None: 31 | """Jinja2 Extension Constructor.""" 32 | super().__init__(environment) 33 | 34 | def random_ascii_string(length: int, punctuation: bool = False) -> str: 35 | if punctuation: 36 | corpus = "".join((string.ascii_letters, string.punctuation)) 37 | else: 38 | corpus = string.ascii_letters 39 | return "".join(choice(corpus) for _ in range(length)) 40 | 41 | environment.globals.update(random_ascii_string=random_ascii_string) # type: ignore 42 | 43 | 44 | class SlugifyExtension(Extension): 45 | """Jinja2 Extension to slugify string.""" 46 | 47 | def __init__(self, environment: Environment) -> None: 48 | """Jinja2 Extension constructor.""" 49 | super().__init__(environment) 50 | 51 | def slugify(value: typing.Any, **kwargs: typing.Any) -> str: 52 | """Slugifies the value.""" 53 | return pyslugify(value, **kwargs) 54 | 55 | environment.filters["slugify"] = slugify # type: ignore 56 | 57 | 58 | class UUIDExtension(Extension): 59 | """Jinja2 Extension to generate uuid4 string.""" 60 | 61 | def __init__(self, environment: Environment) -> None: 62 | """Jinja2 Extension constructor.""" 63 | super().__init__(environment) 64 | 65 | def uuid4() -> str: 66 | """Generate UUID4.""" 67 | return str(uuid.uuid4()) 68 | 69 | environment.globals.update(uuid4=uuid4) # type: ignore 70 | -------------------------------------------------------------------------------- /fabricius/exceptions.py: -------------------------------------------------------------------------------- 1 | class FabriciusError(Exception): 2 | """ 3 | An error was raised inside Fabricius. 4 | 5 | All exceptions of the Fabricius library **must** subclass this exception. 6 | """ 7 | 8 | def __init__(self, error: object | None = None) -> None: 9 | super().__init__(error or "Error inside Fabricius. No specific error raised.") 10 | 11 | 12 | class MissingRequiredValueError(FabriciusError): 13 | """ 14 | Exception raised when a required value was not set inside an object. 15 | """ 16 | 17 | def __init__(self, instance: object, missing_value: str) -> None: 18 | """Parameters 19 | ---------- 20 | instance : object 21 | The object that holds the missing value. 22 | missing_value : str 23 | The value that was not set. 24 | """ 25 | super().__init__( 26 | f"{instance.__class__.__name__} is missing a required value to be set: " 27 | f"'{missing_value}'" 28 | ) 29 | 30 | 31 | class AlreadyCommittedError(FabriciusError): 32 | """ 33 | A file has already been committed/persisted. 34 | """ 35 | 36 | def __init__(self, file_name: str) -> None: 37 | """Parameters 38 | ---------- 39 | file_name : str 40 | The file's name that has already been committed. 41 | """ 42 | super().__init__(f"File '{file_name}' has already been committed.") 43 | 44 | 45 | class ConflictError(FabriciusError): 46 | """ 47 | Conflicting parameters between objects. 48 | 49 | This mean that Fabricius cannot continue its work because there would be a conflict between 50 | one or two objects. 51 | 52 | For example, this error can be raised when two files have the same name in the same 53 | destination folder. 54 | """ 55 | 56 | def __init__(self, instance: object, reason: str) -> None: 57 | """ 58 | Parameters 59 | ---------- 60 | instance : object 61 | The conflicting object. 62 | reason : str 63 | The reason for the conflict. 64 | """ 65 | super().__init__(f"Conflict error: {instance.__class__.__name__}: {reason}") 66 | 67 | 68 | class TemplateError(FabriciusError): 69 | """ 70 | The template has an error that cannot be automatically handled by Fabricius. 71 | """ 72 | 73 | def __init__(self, template_name: str, reason: str) -> None: 74 | """ 75 | Parameters 76 | ---------- 77 | template_name : str 78 | The name of the template that raised the error. 79 | reason : str 80 | The reason for the error. 81 | """ 82 | super().__init__(f"{template_name}: {reason}") 83 | -------------------------------------------------------------------------------- /docs/source/guides/guide_cookiecutter_templates.rst: -------------------------------------------------------------------------------- 1 | Guide: Rendering CookieCutter templates 2 | ======================================= 3 | 4 | Fabricius ships with the ability to process CookieCutter templates (Or so called, cookiecutters by themselves). 5 | This mean that all of the work you've already done using CookieCutter **is 100% supported** in Fabricius! (yes! hooks work too!) 6 | 7 | Using the CLI 8 | ------------- 9 | 10 | TBD 11 | 12 | Using the API 13 | ------------- 14 | 15 | You can also use Fabricius's API to generate your CookieCutter template. 16 | 17 | .. code-block:: python 18 | 19 | from fabricius.readers.cookiecutter.setup import setup, run 20 | 21 | def main(): 22 | template_path = "path/to/template" 23 | output_path = "path/to/output" 24 | template = setup(template_path, output_path) 25 | 26 | # This method has been specially created for CookieCutter's solution. 27 | # See below for explanations. 28 | run(template) 29 | 30 | API 31 | --- 32 | 33 | The following functions are available as part of the little API you can use to generate :py:class:`fabricius.models.template.Template` objects from a CookieCutter repo. 34 | 35 | The ``setup`` function will: 36 | 37 | 1. Get the template path 38 | 2. Create the Template object and add the extensions, if the project templates specify any others, add them too. 39 | 3. Add ``_template``, ``_repo_dir`` and ``_output_dir`` to the context 40 | 4. Obtain the questions in the project template and begin to ask to the users those questions. 41 | 5. Once all answered, add the extra context the user's default context, then add the answers to the final context. 42 | 6. Obtain all the files that must be rendered/copied, and add them to the Template object, and push the data to the Template object. 43 | 7. Connect the hooks to the ``before_template_commit`` and ``after_template_commit`` signals, then return the Template object. 44 | 45 | While you can just simply do :py:meth:`Template.commit() `, there is a few things to considerate first since you're rendering a CookieCutter project, and not a Fabricius one. 46 | Thus, we have made the ``run`` function to handle a few edge cases that could happens with CookieCutter. 47 | 48 | The ``run`` function will: 49 | 50 | 1. First attempt to commit the project 51 | 2. If fail, due to a file that already exist, ask the user if overwriting files should be used. 52 | 3. If fail, due to a hook failing, see if the exception gives an exit code, if it does, exit using the exit code, if not, print the exception and exit. 53 | 4. Return the list of file commit result. 54 | 55 | 56 | .. automodule:: fabricius.readers.cookiecutter.setup 57 | :members: setup, run 58 | :noindex: 59 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "Fabricius" 3 | version = "0.2.0" 4 | description = "Fabricius: The supportive templating engine for Python!" 5 | license = "MIT" 6 | authors = ["Predeactor "] 7 | readme = "README.md" 8 | repository = "https://github.com/Predeactor/Fabricius" 9 | documentation = "https://fabricius.readthedocs.org" 10 | keywords = ["cookiecutter", "template", "project", "scaffold", "pyscaffold"] 11 | classifiers = [ 12 | "Development Status :: 2 - Pre-Alpha", 13 | 14 | "Intended Audience :: Developers", 15 | "Intended Audience :: End Users/Desktop", 16 | 17 | "License :: OSI Approved :: MIT License", 18 | "Natural Language :: English", 19 | 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: Implementation :: CPython", 22 | "Operating System :: OS Independent", 23 | 24 | "Topic :: Software Development :: Libraries :: Python Modules", 25 | "Topic :: Software Development :: Build Tools", 26 | "Topic :: Utilities", 27 | 28 | "Typing :: Typed", 29 | ] 30 | 31 | packages = [ 32 | { include = "fabricius" } 33 | ] 34 | 35 | include = ["README.md", "LICENSE"] 36 | 37 | [tool.poetry.dependencies] 38 | python = "^3.10" 39 | chevron = "^0.14.0" 40 | rich = "^12.4.4" 41 | typing-extensions = "^4.2.0" 42 | inflection = "^0.5.1" 43 | click = "^8.1.3" 44 | jinja2 = "^3.1.2" 45 | jinja2-time = "^0.2.0" 46 | slugify = "^0.0.1" 47 | pyyaml = "^6.0" 48 | platformdirs = "^3.5.1" 49 | 50 | 51 | [tool.poetry.group.docs] 52 | optional = true 53 | 54 | [tool.poetry.group.docs.dependencies] 55 | Sphinx = "^5.0.2" 56 | furo = "^2022.6.21" 57 | sphinx-autobuild = "^2021.3.14" 58 | 59 | [tool.poetry.group.dev] 60 | optional = true 61 | 62 | [tool.poetry.group.dev.dependencies] 63 | black = "^23.3.0" 64 | isort = "^5.12.0" 65 | pre-commit = "^2.19.0" 66 | mypy = "^1.0.0" 67 | tox = "^4.4.5" 68 | types-chevron = "^0.14.2.2" 69 | esbonio = "^0.16.1" 70 | ipykernel = "^6.22.0" 71 | pytest = "^7.3.1" 72 | poethepoet = "^0.20.0" 73 | 74 | # [tool.poetry.commands] 75 | 76 | [tool.poetry.group.actions] 77 | optional = true 78 | 79 | [tool.poetry.group.actions.dependencies] 80 | tox-gh-actions = "^3.0.0" 81 | 82 | [tool.poe] 83 | poetry_command = "" 84 | 85 | [tool.poe.tasks] 86 | _format_black = "black ." 87 | _format_isort = "isort ." 88 | format = ["_format_black", "_format_isort"] 89 | test = "pytest . -v" 90 | 91 | [tool.poe.tasks.docs] 92 | cmd = "make livehtml" 93 | cwd = "./docs" 94 | 95 | [tool.poe.tasks.docscov] 96 | cmd = "make html -b coverage" 97 | cwd = "./docs" 98 | 99 | [tool.black] 100 | line-length = 99 101 | 102 | [tool.isort] 103 | profile = "black" 104 | 105 | [build-system] 106 | requires = ["poetry-core>=1.0.0"] 107 | build-backend = "poetry.core.masonry.api" 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fabricius 2 | 3 | Fabricius - A Python 3.10 Project Template engine with superpowers! 4 | 5 | > :warning: Fabricius is a work in progress! Please, play with it with a grain of salt; expect bugs, crashes, non-documented portion of the application & more unexpected behavior. 6 | 7 | Documentation: 8 | 9 | > :warning: Fabricius still does not comes with it's CLI tool! It is a work in progress! 10 | 11 | > [!CAUTION] 12 | > As of 7 March 2024, this repository has been officially dropped and is **no longer supported**. 13 | > The reasons are: 14 | > - I do not want to deal with Python for my personnal projects. 15 | > - I believe there's no real need for me to keep this project "as alive" when I do not plan to commit ever again. 16 | > 17 | > I feel truly sorry for the person I might disappoint here. I know a few peoples was following the project and had interest. I just don't feel like keeping up with Python and how unsupportive it feels toward my programming goals. 18 | > I also feel disappointed toward myself as, that mean, I never could achieve the goal I was trying to reach for. But this feel like a necessity for me. Farewell, Python. 19 | > 20 | > As such, this repository will now be archived. I hope you understand this decision. If you'd like to, feel free to fork this repository, make changes, and enjoy it yourself. 21 | 22 | ## Goals 23 | 24 | 1. Create a working project from a project template 25 | 2. Create a fully working CLI using Rich 26 | 3. Ability to clone repository and use their templates 27 | 4. Create a secure tool (Do not allow unsecure scripts) 28 | 5. Create a fully type hinted tool 29 | 30 | ## Why the name of "Fabricius"? 31 | 32 | I am an immense fan of roman names, and I very often name my project after a meaningful roman name. 33 | 34 | "Fabricius" (In French, "Artisan") is translated to "craftsman", which is what Fabricius, the tool we create, aims to. His goal is to help at its best to create your projects easily. 35 | 36 | ## Why not just use CookieCutter or Copier instead of creating your own tool? 37 | 38 | See goals, but other than that, 39 | 40 | It's a question I am expecting with fears, I tried to first use CookieCutter myself but I never liked it at all, it always broke with me and was painful to use. On top of that, it does not comes with crucial things I would personally require, such as basic type checking when gathering user's input. 41 | As for Copier, while it seems like a much more grown-up tool and *actually* fitting my need, I honestly did not try it, I just lost interested towards it and wanted to challenge myself in creating a new tool for the Python ecosystem. 42 | 43 | On top of all of these, during my work in 2022, I ended up using TypeScript and using AdonisJS's CLI tool, and its awesome [template generator](https://docs.adonisjs.com/guides/ace-commandline#templates-generator), and so it made me really interested into creating a project scaffolder but using code, not a directory structure, which was lacking for both tools. 44 | 45 | I wanted to create a complete and customizable experience of project scaffolding, I wanted to allow users to be free of do whatever they've meant to do, it's how I came up with the idea of plugins. 46 | 47 | To me, Fabricius is more than just a simple project scaffolder, it's a complete handy swiss knife for their users. :) 48 | -------------------------------------------------------------------------------- /fabricius/readers/cookiecutter/hooks.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import pathlib 3 | import subprocess 4 | import sys 5 | import tempfile 6 | import typing 7 | 8 | from fabricius.models.file import FileCommitResult 9 | from fabricius.models.template import Template 10 | from fabricius.readers.cookiecutter.exceptions import FailedHookError 11 | from fabricius.renderers.jinja_renderer import JinjaRenderer 12 | from fabricius.types import Data 13 | 14 | HOOKS = ["pre_gen_project", "post_gen_project"] 15 | 16 | 17 | class AvailableHooks(typing.TypedDict): 18 | pre_gen_project: typing.Optional[pathlib.Path] 19 | post_gen_project: typing.Optional[pathlib.Path] 20 | 21 | 22 | def get_hooks(base_folder: pathlib.Path) -> AvailableHooks | None: 23 | hooks_folder = base_folder.joinpath("hooks") 24 | if not hooks_folder.exists(): 25 | return None 26 | 27 | available_hooks: AvailableHooks = {"post_gen_project": None, "pre_gen_project": None} 28 | for hook_file in hooks_folder.iterdir(): 29 | # annoying mypy moment... requiring literal string... 30 | if hook_file.stem == "post_gen_project": 31 | available_hooks["post_gen_project"] = hook_file 32 | if hook_file.stem == "pre_gen_project": 33 | available_hooks["pre_gen_project"] = hook_file 34 | 35 | return None if len(available_hooks) == 0 else available_hooks 36 | 37 | 38 | def run_hook(hook: pathlib.Path, data: Data): 39 | # Renderer the file 40 | with tempfile.NamedTemporaryFile( 41 | delete=False, suffix=hook.suffix, mode="wb" 42 | ) as temporary_file: 43 | final_content = JinjaRenderer(data).render(hook.read_text()) 44 | temporary_file.write(final_content.encode("utf-8")) 45 | 46 | path = pathlib.Path(temporary_file.name).resolve() 47 | cmd = [sys.executable, str(path)] if hook.suffix == ".py" else [str(path)] 48 | 49 | try: 50 | proc_exit = subprocess.Popen(cmd, shell=sys.platform.startswith("win"), cwd=".").wait(10) 51 | if proc_exit != 0: 52 | raise FailedHookError(hook.name, f"Exit status: {proc_exit}") 53 | except OSError as exception: 54 | if exception.errno == errno.ENOEXEC: 55 | raise FailedHookError( 56 | hook.name, "Might be an empty file or missing a shebang" 57 | ) from exception 58 | raise FailedHookError(f"Exception: {exception}") from exception 59 | 60 | 61 | @typing.overload 62 | def adapt( 63 | hook: pathlib.Path, type: typing.Literal["pre"] 64 | ) -> typing.Callable[[Template[typing.Any]], typing.Any]: 65 | ... 66 | 67 | 68 | @typing.overload 69 | def adapt( 70 | hook: pathlib.Path, type: typing.Literal["post"] 71 | ) -> typing.Callable[[Template[typing.Any], list[FileCommitResult]], typing.Any]: 72 | ... 73 | 74 | 75 | def adapt( 76 | hook: pathlib.Path, type: typing.Literal["pre", "post"] 77 | ) -> ( 78 | typing.Callable[[Template[typing.Any]], typing.Any] 79 | | typing.Callable[[Template[typing.Any], list[FileCommitResult]], typing.Any] 80 | ): 81 | if type == "pre": 82 | 83 | def pre_wrapper(template: Template[typing.Any]): 84 | run_hook(hook, template.data) 85 | 86 | return pre_wrapper 87 | 88 | if type == "post": 89 | 90 | def post_wrapper(template: Template[typing.Any], files_commit: list[FileCommitResult]): 91 | run_hook(hook, template.data) 92 | 93 | return post_wrapper 94 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | import fabricius 17 | 18 | sys.path.insert(0, os.path.abspath("../..")) 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "Fabricius" 23 | copyright = "2022, Predeactor" 24 | author = "Predeactor" 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = fabricius.__version__ 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.napoleon", 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.intersphinx", 38 | "sphinx.ext.coverage", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns: "list[str]" = [] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | html_theme = "furo" 56 | html_css_files = [ 57 | "furo_custom.css", 58 | ] 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ["_static"] 64 | 65 | 66 | # -- Extension configuration ------------------------------------------------- 67 | coverage_show_missing_items = True 68 | 69 | # -- Options for Furo theme -------------------------------------------------- 70 | announcement = "⚠️ Fabricius is a work in progress, the documentation only shows what will be realizable in the future with Fabricius as a tool.
Guides are only here to show you what will be possible, and they might not be reproducible as of today." 71 | html_theme_options = { 72 | "light_logo": "logo-dark.png", 73 | "dark_logo": "logo-white.png", 74 | "announcement": announcement, 75 | } 76 | 77 | 78 | # -- Options for autodoc extension ------------------------------------------ 79 | autodoc_default_options = { 80 | "member-order": "bysource", 81 | } 82 | autoclass_content = "both" 83 | 84 | 85 | # -- Options for coverage extension ------------------------------------------ 86 | coverage_show_missing_items = True 87 | 88 | # -- Options for intersphinx extension --------------------------------------- 89 | intersphinx_mapping = { 90 | "py": ("https://docs.python.org/3", None), 91 | "rich": ("https://rich.readthedocs.io/en/stable", None), 92 | } 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | tests/results/* 54 | !tests/results/.gitkeep 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | .pybuilder/ 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # poetry 100 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 101 | # This is especially recommended for binary packages to ensure reproducibility, and is more 102 | # commonly ignored for libraries. 103 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 104 | #poetry.lock 105 | 106 | # pdm 107 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 108 | #pdm.lock 109 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 110 | # in version control. 111 | # https://pdm.fming.dev/#use-with-ide 112 | .pdm.toml 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | .idea/ 163 | 164 | # VSCode 165 | .vscode/* 166 | !.vscode/extensions.json 167 | !.vscode/launch.json 168 | !.vscode/tasks.json 169 | -------------------------------------------------------------------------------- /fabricius/models/template.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import typing 3 | 4 | from typing_extensions import Self 5 | 6 | from fabricius.app.signals import after_template_commit, before_template_commit 7 | from fabricius.exceptions import ( 8 | AlreadyCommittedError, 9 | ConflictError, 10 | MissingRequiredValueError, 11 | ) 12 | from fabricius.models.file import File, FileCommitResult 13 | from fabricius.models.renderer import Renderer 14 | from fabricius.types import Data, PathStrOrPath 15 | 16 | STATE = typing.Literal["pending", "failed", "persisted"] 17 | RendererType = typing.TypeVar("RendererType", bound=type[Renderer]) 18 | 19 | 20 | class Template(typing.Generic[RendererType]): 21 | """ 22 | The :py:class:`Template` class represent "a collection of files that, in its whole, represents 23 | a project" 24 | 25 | The difference between the :py:class:`.Template` class and a collection of :py:class:`.File` 26 | is that a template assumes all of your files have the same properties. (Requires the same 27 | renderer, the same data, etc.) 28 | 29 | Typically, a template only use one renderer, and shares the same data across the whole project 30 | template. 31 | 32 | The :py:class:`.Template` will assist creating a project, while providing a similar interface 33 | of the :py:class:`.File` model. 34 | """ 35 | 36 | state: STATE 37 | """ 38 | The state of the template 39 | """ 40 | 41 | base_folder: pathlib.Path 42 | """ 43 | The folder where the template will be generated. 44 | """ 45 | 46 | files: list[File] 47 | """ 48 | The list of files that will be rendered when committing. 49 | """ 50 | 51 | data: Data 52 | """ 53 | The data to pass to the files. 54 | """ 55 | 56 | renderer: RendererType 57 | """ 58 | The renderer that will be used to generate the files. 59 | """ 60 | 61 | _will_fake: bool 62 | 63 | def __init__( 64 | self, 65 | base_folder: PathStrOrPath, 66 | renderer: RendererType, 67 | ) -> None: 68 | """ 69 | Parameters 70 | ---------- 71 | base_folder : :py:const:`fabricius.types.PathStrOrPath` 72 | Indication of where the template should be generated. 73 | renderer : Type of :py:class:`fabricius.models.renderer.Renderer` 74 | The renderer to use with the template. 75 | """ 76 | self.base_folder = pathlib.Path(base_folder) 77 | self.state = "pending" 78 | self.files = [] 79 | self.data = {} 80 | self.renderer = renderer 81 | self._will_fake = False 82 | 83 | @property 84 | def __files_destinations(self) -> list[pathlib.Path | None]: 85 | return [file.destination for file in self.files] 86 | 87 | def add_file(self, file: File) -> Self: 88 | if not file.can_commit: 89 | reason = file.can_commit 90 | if reason == "state": 91 | raise AlreadyCommittedError(file.name) 92 | raise MissingRequiredValueError(self, reason) 93 | 94 | if file.destination and file.compute_destination() in self.__files_destinations: 95 | raise ConflictError( 96 | file, 97 | f"File {file.name} has a destination that already is present in Template's destinations.", 98 | ) 99 | 100 | self.files.append(file) 101 | return self 102 | 103 | def add_files(self, files: typing.Iterable[File]) -> Self: 104 | for file in files: 105 | self.add_file(file) 106 | return self 107 | 108 | def push_data(self, data: Data) -> Self: 109 | self.data = data 110 | return self 111 | 112 | def fake(self) -> Self: 113 | self._will_fake = True 114 | return self 115 | 116 | def restore(self) -> Self: 117 | self._will_fake = False 118 | return self 119 | 120 | def commit(self, *, overwrite: bool = False) -> list[FileCommitResult]: 121 | results: list[FileCommitResult] = [] 122 | 123 | before_template_commit.send(self) 124 | 125 | for file in self.files: 126 | file.with_data(self.data, overwrite=False) 127 | if self._will_fake: 128 | file.fake() 129 | else: 130 | # Just in case they've been set to fake... 131 | file.restore() 132 | result = file.commit(overwrite=overwrite) 133 | results.append(result) 134 | 135 | after_template_commit.send(self, results) 136 | 137 | return results 138 | -------------------------------------------------------------------------------- /tests/test_file.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import unittest 3 | 4 | from fabricius.models.file import AlreadyCommittedError, File 5 | from fabricius.renderers import ( 6 | ChevronRenderer, 7 | PythonFormatRenderer, 8 | StringTemplateRenderer, 9 | ) 10 | 11 | 12 | class TestFile(unittest.TestCase): 13 | """ 14 | Test Fabricius's File. 15 | """ 16 | 17 | TEMPLATE_PATH = pathlib.Path(__file__, "..", "files", "templates").resolve() 18 | DESTINATION_PATH = pathlib.Path(__file__, "..", "results", "file").resolve() 19 | 20 | def test_file_name(self): 21 | """ 22 | Test File's proper name. 23 | """ 24 | file = File("my_file_name") 25 | self.assertEqual(file.name, "my_file_name") 26 | file = File("my_file_name", "txt") 27 | self.assertEqual(file.name, "my_file_name.txt") 28 | 29 | def test_file_state(self): 30 | """ 31 | Test File's proper state. 32 | """ 33 | file = File("test") 34 | self.assertEqual(file.state, "pending") 35 | 36 | def test_file_content(self): 37 | """ 38 | Test File's proper content usage. 39 | """ 40 | file = File("test", "txt") 41 | file_content = self.TEMPLATE_PATH.joinpath("python_template.txt").read_text() 42 | 43 | with self.assertRaises(FileNotFoundError): 44 | file.from_file(self.TEMPLATE_PATH.joinpath("idonotexist.txt")) 45 | 46 | file.from_file(self.TEMPLATE_PATH.joinpath("python_template.txt")) 47 | self.assertEqual(file_content, file.content) 48 | 49 | file.from_content("Hello! I am {name} with some content") 50 | self.assertEqual("Hello! I am {name} with some content", file.content) 51 | 52 | def test_file_destination(self): 53 | """ 54 | Test File's proper destination. 55 | """ 56 | file = File("test", "txt") 57 | 58 | file.to_directory(self.DESTINATION_PATH) 59 | self.assertEqual(str(self.DESTINATION_PATH), str(file.destination)) 60 | 61 | with self.assertRaises(NotADirectoryError): 62 | file.to_directory(__file__) 63 | 64 | def test_file_renderer(self): 65 | """ 66 | Test File's proper renderer. 67 | """ 68 | file = File("test", "txt") 69 | self.assertIs(file.renderer, PythonFormatRenderer) 70 | 71 | file.use_mustache() 72 | self.assertIs(file.renderer, ChevronRenderer) 73 | 74 | file.use_string_template() 75 | self.assertIs(file.renderer, StringTemplateRenderer) 76 | 77 | file.with_renderer(PythonFormatRenderer) 78 | self.assertIs(file.renderer, PythonFormatRenderer) 79 | 80 | def test_file_data(self): 81 | """ 82 | Test File's proper data. 83 | """ 84 | file = File("test", "txt") 85 | 86 | file.with_data({"some": "data"}) 87 | self.assertDictEqual(file.data, {"some": "data"}) 88 | 89 | file.with_data({"new": "data"}) 90 | self.assertDictEqual(file.data, {"new": "data"}) 91 | 92 | file.with_data({"more": "new data"}, overwrite=False) 93 | self.assertDictEqual(file.data, {"new": "data", "more": "new data"}) 94 | 95 | def test_file_fake(self): 96 | """ 97 | Test File's fake. 98 | """ 99 | path = self.DESTINATION_PATH.joinpath("should", "not", "exist") 100 | file = File("test", "txt") 101 | 102 | file.from_content("Should not be generated").to_directory(path) 103 | result = file.fake().commit() 104 | 105 | self.assertIs(result["state"], "persisted") 106 | self.assertFalse(path.joinpath("test.txt").exists()) 107 | 108 | def test_file_generate(self): 109 | """ 110 | Test File's proper generation. 111 | """ 112 | file = File("test", "txt") 113 | 114 | file.from_content("My name is {name}!") 115 | file.with_data({"name": "Python"}) 116 | result = file.generate() 117 | 118 | self.assertEqual(result, "My name is Python!") 119 | 120 | def test_file_commit(self): 121 | """ 122 | Test File's proper commit. 123 | """ 124 | file = File("python_result", "txt") 125 | 126 | file.from_file(self.TEMPLATE_PATH.joinpath("python_template.txt")).to_directory( 127 | self.DESTINATION_PATH 128 | ).with_data({"name": "Python's format"}) 129 | 130 | result = file.commit(overwrite=True) 131 | self.assertIsInstance(result, dict) 132 | self.assertTrue(self.DESTINATION_PATH.joinpath("python_result.txt").exists()) 133 | self.assertEqual(file.state, "persisted") 134 | 135 | with self.assertRaises(AlreadyCommittedError): 136 | file.commit() 137 | 138 | with self.assertRaises(FileExistsError): 139 | file = File("python_result", "txt") 140 | file.from_file(self.TEMPLATE_PATH.joinpath("python_template.txt")).to_directory( 141 | self.DESTINATION_PATH 142 | ).with_data({"name": "Python's format"}) 143 | file.commit() 144 | -------------------------------------------------------------------------------- /fabricius/utils.py: -------------------------------------------------------------------------------- 1 | import random 2 | import typing 3 | 4 | import inflection 5 | from rich.color import Color 6 | 7 | FABRICIUS_IS_AWESOME = [ 8 | "Generating your project, hang tight!", 9 | "Bing, bonk, bang! Scaffolding your next idea...", 10 | "Helping you on your next idea!", 11 | "Serious work in progress... Give it a second or two!", 12 | ] 13 | 14 | 15 | def fetch_me_a_beer() -> str: 16 | return random.choice(FABRICIUS_IS_AWESOME) 17 | 18 | 19 | def calculate_text_color( 20 | color: str | Color, *, threshold: int = 150 21 | ) -> typing.Literal["black", "bright_white"]: 22 | """ 23 | Calculate if the text should be in black or white depending on a color. 24 | It uses the following formula: (r * 0.299 + g * 0.587 + b * 0.114) > threshold 25 | Where r, g, and b are the RGB (triplet) values of the color. 26 | 27 | Parameters 28 | ---------- 29 | color : :py:class:`str` or :py:class:`rich.Color` 30 | The color we're calculating against to. 31 | This must be a Color class (From Rich) 32 | threshold : :py:class:`int` 33 | The threshold to determine if the text is black or white. 34 | 35 | Raises 36 | ------ 37 | :py:exc:`rich.color.ColorParseError` : 38 | If ``color`` is of type :py:class:`str`, this will try to parse the color, this error 39 | will be raised if failed to be parsed. 40 | 41 | Returns 42 | ------- 43 | str : 44 | Return either "black" or "bright_white" 45 | """ 46 | if isinstance(color, str): 47 | color = Color.parse(color) 48 | r, g, b = tuple(color.get_truecolor()) 49 | 50 | return "black" if (r * 0.299 + g * 0.587 + b * 0.114) > threshold else "bright_white" 51 | 52 | 53 | def camel_case(text: str) -> str: 54 | """ 55 | Return the text formatted in camel case 56 | 57 | Parameters 58 | ---------- 59 | text : :py:class:`str` 60 | The text you want to format. 61 | 62 | Returns 63 | ------- 64 | :py:class:`str` : 65 | The formatted text. 66 | 67 | Example 68 | ------- 69 | .. code-block:: python 70 | 71 | >>> my_text = "Some text" 72 | >>> camel_case(my_text) 73 | 'someText' 74 | """ 75 | return inflection.camelize(text, False) 76 | 77 | 78 | def snake_case(text: str) -> str: 79 | """ 80 | Return the text formatted in snake case 81 | 82 | Parameters 83 | ---------- 84 | text : :py:class:`str` 85 | The text you want to format. 86 | 87 | Returns 88 | ------- 89 | :py:class:`str` : 90 | The formatted text. 91 | 92 | Example 93 | ------- 94 | .. code-block:: python 95 | 96 | >>> my_text = "Some text" 97 | >>> snake_case(my_text) 98 | 'some_text' 99 | """ 100 | return inflection.underscore(text) 101 | 102 | 103 | def dash_case(text: str) -> str: 104 | """ 105 | Return the text formatted in dash case 106 | 107 | Parameters 108 | ---------- 109 | text : :py:class:`str` 110 | The text you want to format. 111 | 112 | Returns 113 | ------- 114 | :py:class:`str` : 115 | The formatted text. 116 | 117 | Example 118 | ------- 119 | .. code-block:: python 120 | 121 | >>> my_text = "Some text" 122 | >>> dash_case(my_text) 123 | 'some-text' 124 | """ 125 | return inflection.dasherize(text) 126 | 127 | 128 | def pascal_case(text: str) -> str: 129 | """ 130 | Return the text formatted in pascal case 131 | 132 | Parameters 133 | ---------- 134 | text : :py:class:`str` 135 | The text you want to format. 136 | 137 | Returns 138 | ------- 139 | :py:class:`str` : 140 | The formatted text. 141 | 142 | Example 143 | ------- 144 | .. code-block:: python 145 | 146 | >>> my_text = "Some text" 147 | >>> pascal_case(my_text) 148 | 'SomeText' 149 | """ 150 | return inflection.camelize(text, True) 151 | 152 | 153 | def capital_case(text: str) -> str: 154 | """ 155 | Return the text formatted in capital case 156 | 157 | Parameters 158 | ---------- 159 | text : :py:class:`str` 160 | The text you want to format. 161 | 162 | Returns 163 | ------- 164 | :py:class:`str` : 165 | The formatted text. 166 | 167 | Example 168 | ------- 169 | .. code-block:: python 170 | 171 | >>> my_text = "Some text" 172 | >>> capital_case(my_text) 173 | 'Some Text' 174 | """ 175 | return inflection.titleize(text) 176 | 177 | 178 | def sentence_case(text: str) -> str: 179 | """ 180 | Return the text formatted in sentence case 181 | 182 | Parameters 183 | ---------- 184 | text : :py:class:`str` 185 | The text you want to format. 186 | 187 | Returns 188 | ------- 189 | :py:class:`str` : 190 | The formatted text. 191 | 192 | Example 193 | ------- 194 | .. code-block:: python 195 | 196 | >>> my_text = "Some text" 197 | >>> sentence_case(my_text) 198 | 'Some text' 199 | """ 200 | has_ending_id = bool(text.endswith("_id")) 201 | result = inflection.humanize(text) 202 | if has_ending_id: 203 | result += " ID" 204 | return result 205 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [pro.julien.mauroy@gmail.com](mailto:pro.julien.mauroy@gmail.com). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /fabricius/readers/cookiecutter/setup.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import sys 4 | import typing 5 | from fnmatch import fnmatch 6 | from functools import partial 7 | 8 | from rich import get_console 9 | from rich.prompt import Confirm, Prompt 10 | 11 | from fabricius.app.signals import after_template_commit, before_template_commit 12 | from fabricius.app.ui import TemplateProgressBar 13 | from fabricius.exceptions import TemplateError 14 | from fabricius.models.file import File 15 | from fabricius.models.renderer import Renderer 16 | from fabricius.models.template import Template 17 | from fabricius.readers.cookiecutter.config import get_config 18 | from fabricius.readers.cookiecutter.exceptions import FailedHookError 19 | from fabricius.readers.cookiecutter.hooks import adapt, get_hooks 20 | from fabricius.renderers.jinja_renderer import JinjaRenderer 21 | from fabricius.types import FileCommitResult, PathStrOrPath 22 | from fabricius.utils import fetch_me_a_beer, sentence_case 23 | 24 | EXTENSIONS = [ 25 | "fabricius.readers.cookiecutter.extensions.JsonifyExtension", 26 | "fabricius.readers.cookiecutter.extensions.RandomStringExtension", 27 | "fabricius.readers.cookiecutter.extensions.SlugifyExtension", 28 | "fabricius.readers.cookiecutter.extensions.UUIDExtension", 29 | "jinja2_time.TimeExtension", 30 | ] 31 | 32 | 33 | Context = typing.NewType("Context", dict[str, typing.Any]) 34 | QuestionContext = typing.NewType("QuestionContext", dict[str, typing.Any]) 35 | CookieContext = typing.NewType("CookieContext", dict[str, typing.Any]) 36 | 37 | 38 | class CopyRender(Renderer): 39 | def render(self, content: str) -> str: 40 | return content 41 | 42 | 43 | def obtain_template_path(base_folder: pathlib.Path) -> pathlib.Path | None: 44 | return next( 45 | ( 46 | path 47 | for path in base_folder.iterdir() 48 | if "cookiecutter" in path.name and "{{" in path.name and "}}" in path.name 49 | ), 50 | None, 51 | ) 52 | 53 | 54 | def wrap_in_cookie(data: Context) -> CookieContext: 55 | return CookieContext({"cookiecutter": data}) 56 | 57 | 58 | def obtain_files( 59 | base_folder: pathlib.Path, output_folder: pathlib.Path, data: CookieContext 60 | ) -> list[File]: 61 | files: list[File] = [] 62 | for file_path in base_folder.iterdir(): 63 | if file_path.is_file(): 64 | if "{{" in file_path.name and "}}" in file_path.name: 65 | file_name = JinjaRenderer(data).render(file_path.name) 66 | else: 67 | file_name = file_path.name 68 | file = File(file_name).from_file(file_path).to_directory(output_folder) 69 | if should_copy_not_render(file, data): 70 | file.with_renderer(CopyRender) 71 | else: 72 | file.use_jinja() 73 | files.append(file) 74 | return files 75 | 76 | 77 | def should_copy_not_render(file: File, context: CookieContext) -> bool: 78 | if not context["cookiecutter"].get("_copy_without_render"): 79 | return False 80 | to_ignore: list[str] = context["cookiecutter"]["_copy_without_render"] 81 | for index, value in enumerate(to_ignore): 82 | # Render the string 83 | to_ignore[index] = JinjaRenderer(context).render(value) 84 | return any(fnmatch(str(file.compute_destination()), value) for value in to_ignore) 85 | 86 | 87 | def read_context_raw(file: pathlib.Path) -> Context: 88 | if not file.exists(): 89 | raise TemplateError(file.parent.name, f"{file.name} does not exist") 90 | content = file.read_text() 91 | 92 | try: 93 | context_data = json.loads(content) 94 | except json.JSONDecodeError as exception: 95 | raise TemplateError( 96 | file.parent.name, f"{file.name} does not appear to be a valid JSON file" 97 | ) from exception 98 | 99 | return Context(context_data) 100 | 101 | 102 | def get_questions_only(context: Context) -> QuestionContext: 103 | question_context = {key: value for key, value in context.items() if not key.startswith("_")} 104 | return QuestionContext(question_context) 105 | 106 | 107 | def get_answer(prompts: QuestionContext, *, no_prompt: bool = False) -> dict[str, typing.Any]: 108 | answers: dict[str, typing.Any] = {} 109 | 110 | for question, default_value in prompts.items(): 111 | if isinstance(default_value, list): 112 | default_value = typing.cast(list[str], default_value) 113 | prompt = partial(Prompt(sentence_case(question), choices=default_value)) 114 | else: 115 | prompt = partial(Prompt(sentence_case(question)), default=default_value) 116 | answer = "" if no_prompt else prompt() 117 | answers[question] = answer 118 | 119 | return answers 120 | 121 | 122 | def setup( 123 | base_folder: PathStrOrPath, 124 | output_folder: PathStrOrPath, 125 | *, 126 | extra_context: dict[str, typing.Any] | None = None, 127 | no_prompt: bool = False, 128 | ) -> Template[type[JinjaRenderer]]: 129 | """Setup a template that will be able to be ran once created. 130 | 131 | Parameters 132 | ---------- 133 | base_folder : :py:const:`PathStrOrPath ` 134 | The folder where the template is located. (Choose the folder where the ``cookiecutter.json`` 135 | is located, not the template itself) 136 | output_folder : :py:const:`PathStrOrPath ` 137 | The folder where the template/files will be created once rendered. 138 | extra_context : :py:const:`Data `, optional 139 | Any extra context to pass to the template. 140 | It will override the user's prompt. 141 | no_prompt : bool, optional 142 | If set to True, no questions will be asked to the user. By default False 143 | 144 | Returns 145 | ------- 146 | Type of :py:class:`fabricius.models.template.Template` 147 | The Template that has been generated. 148 | It is ready to be committed, and everything has been setup. 149 | 150 | Raises 151 | ------ 152 | :py:exc:`fabricius.exceptions.TemplateError` 153 | Exception raised when there's an issue with the template that is most probably due to the 154 | template's misconception. 155 | """ 156 | if extra_context is None: 157 | extra_context = {} 158 | 159 | # Obtain the required information first 160 | base_folder = pathlib.Path(base_folder).resolve() 161 | output_folder = pathlib.Path(output_folder).resolve() 162 | 163 | # Prepare contexts 164 | cookiecutter_config_path = base_folder.joinpath("cookiecutter.json") 165 | user_config = get_config() 166 | 167 | # Ensure a cookiecutter.json file exists. 168 | # Obtains the context's raw content & the template's hooks. 169 | if not cookiecutter_config_path.exists(): 170 | raise TemplateError(base_folder.name, "cookiecutter.json does not exist") 171 | context = read_context_raw(cookiecutter_config_path) 172 | hooks = get_hooks(base_folder) 173 | 174 | # Obtain the location of the template, if any. 175 | template_folder = obtain_template_path(base_folder) 176 | if not template_folder: 177 | raise TemplateError(base_folder.name, "No template found") 178 | 179 | # Get the template object 180 | template = Template(template_folder, JinjaRenderer) 181 | for extension in EXTENSIONS: 182 | template.renderer.environment.add_extension(extension) 183 | if context.get("_extensions"): 184 | for extension in context["_extensions"]: 185 | template.renderer.environment.add_extension(extension) 186 | 187 | # Add some additional context 188 | final_context = wrap_in_cookie(context) 189 | final_context["cookiecutter"] |= { 190 | "_template": str(template_folder.resolve()), 191 | "_repo_dir": str(base_folder.resolve()), 192 | "_output_dir": str(output_folder.resolve()), 193 | } 194 | 195 | # Begin to get user's prompts. 196 | questions = get_questions_only(context) 197 | prompts = get_answer(questions, no_prompt=no_prompt) 198 | 199 | prompts.update(extra_context) 200 | 201 | final_context["cookiecutter"].update(user_config["default_context"]) 202 | final_context["cookiecutter"].update(dict(prompts.items())) 203 | 204 | files = obtain_files(template_folder, output_folder, final_context) 205 | template.add_files(files) 206 | template.push_data(final_context) 207 | 208 | if hooks: 209 | if hook_path := hooks["pre_gen_project"]: 210 | before_template_commit.connect(adapt(hook_path, "pre")) # type: ignore 211 | if hook_path := hooks["post_gen_project"]: 212 | after_template_commit.connect(adapt(hook_path, "post")) # type: ignore 213 | 214 | return template 215 | 216 | 217 | def run(template: Template[type[JinjaRenderer]]) -> list[FileCommitResult]: 218 | """Run the CookieCutter template generated using :py:func:`.setup` 219 | 220 | Parameters 221 | ---------- 222 | template : Type of :py:class:`fabricius.models.template.Template` 223 | The template to render. 224 | """ 225 | 226 | def attempt(force: bool) -> list[FileCommitResult]: 227 | progress = TemplateProgressBar(len(template.files)) 228 | with progress.begin(fetch_me_a_beer()): 229 | return template.commit(overwrite=force) 230 | 231 | try: 232 | return attempt(False) 233 | except FileExistsError as exception: 234 | answer = Confirm.ask( 235 | f"File [cyan]{exception.filename}[/] already exists, this probably means that this template has already been created. Overwrite?" 236 | ) 237 | if answer: 238 | return attempt(True) 239 | except FailedHookError as exception: 240 | if exception.exit_code: 241 | sys.exit(exception.exit_code) 242 | else: 243 | get_console().print(exception) 244 | sys.exit(1) 245 | return [] 246 | -------------------------------------------------------------------------------- /fabricius/models/file.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import pathlib 3 | import typing 4 | 5 | from typing_extensions import Self 6 | 7 | from fabricius.app.signals import ( 8 | after_file_commit, 9 | before_file_commit, 10 | on_file_commit_fail, 11 | ) 12 | from fabricius.exceptions import AlreadyCommittedError, MissingRequiredValueError 13 | from fabricius.models.renderer import Renderer 14 | from fabricius.renderers import ( 15 | ChevronRenderer, 16 | JinjaRenderer, 17 | PythonFormatRenderer, 18 | StringTemplateRenderer, 19 | ) 20 | from fabricius.types import FILE_STATE, Data, FileCommitResult, PathStrOrPath 21 | 22 | 23 | class File: 24 | """ 25 | The builder class to initialize a file template. 26 | The result (Through the :py:meth:`.generate` method) is the render of the file's content. 27 | You can "commit" the file to the disk to persist the file's content. 28 | """ 29 | 30 | name: str 31 | """ 32 | The name of the file that will be generated. 33 | """ 34 | 35 | state: FILE_STATE 36 | """ 37 | The state of the file. 38 | """ 39 | 40 | content: str | None 41 | """ 42 | The template's content. 43 | """ 44 | 45 | template_content: str | None 46 | """ 47 | The content of the base template, if set. 48 | """ 49 | 50 | destination: pathlib.Path | None 51 | """ 52 | The destination of the file, if set. 53 | """ 54 | 55 | renderer: type[Renderer] 56 | """ 57 | The renderer to use to generate the file. 58 | """ 59 | 60 | data: Data 61 | """ 62 | The data that will be passed to the renderer. 63 | """ 64 | 65 | _will_fake: bool 66 | """ 67 | If the file should fake its creation upon commit. 68 | """ 69 | 70 | def __init__(self, name: str, extension: typing.Optional[str] = None) -> None: 71 | """ 72 | Parameters 73 | ---------- 74 | name : :py:class:`str` 75 | The name of the file. 76 | extension : :py:class:`str` 77 | The extension of the file, without dot, same as ``name="."`` (Where 78 | ```` and ```` are the arguments given to the class). 79 | """ 80 | self.name = f"{name}.{extension}" if extension else name 81 | self.state = "pending" 82 | self.content = None 83 | self.destination = None 84 | self._will_fake = False 85 | 86 | self.renderer = PythonFormatRenderer 87 | self.data = {} 88 | 89 | def compute_destination(self) -> pathlib.Path: 90 | """ 91 | Compute the destination of the file. 92 | 93 | Raises 94 | ------ 95 | :py:exc:`fabricius.exceptions.MissingRequiredValueError` : 96 | If the object does not have the property "destination" set. 97 | (Use :py:meth:`.to_directory`) 98 | 99 | Returns 100 | ------- 101 | pathlib.Path : 102 | The final path. 103 | """ 104 | if not self.destination: 105 | raise MissingRequiredValueError(self, "destination") 106 | 107 | if not self.destination.exists() and (not self._will_fake): 108 | self.destination.mkdir(parents=True) 109 | return self.destination.joinpath(self.name) 110 | 111 | @property 112 | def can_commit(self) -> typing.Literal["destination", "content", "state", True]: 113 | # sourcery skip: reintroduce-else 114 | if not self.destination: 115 | return "destination" 116 | if not self.content: 117 | return "content" 118 | if self.state == "persisted": 119 | return "state" 120 | 121 | return True 122 | 123 | def from_file(self, path: str | pathlib.Path) -> Self: 124 | """ 125 | Read the content from a file template. 126 | 127 | Raises 128 | ------ 129 | :py:exc:`FileNotFoundError` : 130 | If the file was not found. 131 | 132 | Parameters 133 | ---------- 134 | path : :py:class:`str` or :py:class:`pathlib.Path` 135 | The path of the file template. 136 | """ 137 | path = pathlib.Path(path).resolve() 138 | self.content = path.read_text() 139 | return self 140 | 141 | def from_content(self, content: str) -> Self: 142 | """ 143 | Read the content from a string. 144 | 145 | Parameters 146 | ---------- 147 | content : :py:class:`str` 148 | The template you want to format. 149 | """ 150 | self.content = content 151 | return self 152 | 153 | def to_directory(self, directory: PathStrOrPath) -> Self: 154 | """ 155 | Set the directory where the file will be saved. 156 | 157 | Raises 158 | ------ 159 | :py:exc:`NotADirectory` : 160 | The given path exists but is not a directory. 161 | 162 | Parameters 163 | ---------- 164 | directory : :py:class:`str` or :py:class:`pathlib.Path` 165 | Where the file will be stored. Does not include the file's name. 166 | """ 167 | path = pathlib.Path(directory).resolve() 168 | if path.exists() and not path.is_dir(): 169 | raise NotADirectoryError(f"{path} is not a directory.") 170 | self.destination = path 171 | return self 172 | 173 | def use_mustache(self) -> Self: 174 | """ 175 | Use chevron (Mustache) to render the template. 176 | """ 177 | self.renderer = ChevronRenderer 178 | return self 179 | 180 | def use_string_template(self) -> Self: 181 | """ 182 | Use string.Template to render the template. 183 | """ 184 | self.renderer = StringTemplateRenderer 185 | return self 186 | 187 | def use_jinja(self) -> Self: 188 | """ 189 | Use Jinja2 to render the template. 190 | """ 191 | self.renderer = JinjaRenderer 192 | return self 193 | 194 | def with_renderer(self, renderer: typing.Type[Renderer]) -> Self: 195 | """ 196 | Use a custom renderer to render the template. 197 | 198 | Parameters 199 | ---------- 200 | renderer : Type of :py:class:`fabricius.models.renderer.Renderer` 201 | The renderer to use to format the file. 202 | It must be not initialized. 203 | """ 204 | self.renderer = renderer 205 | return self 206 | 207 | def with_data(self, data: Data, *, overwrite: bool = True) -> Self: 208 | """ 209 | Add data to pass to the template. 210 | 211 | Parameters 212 | ---------- 213 | data : :py:const:`fabricius.types.Data` 214 | The data you want to pass to the template. 215 | overwrite : :py:class:`bool` 216 | If the data that already exists should be deleted. If False, the new data will be 217 | added on top of the already existing data. Default to ``True``. 218 | """ 219 | if overwrite: 220 | self.data = {} 221 | self.data.update(data) 222 | return self 223 | 224 | def fake(self) -> Self: 225 | """ 226 | Set the file to fake the commit. 227 | This will ensure that the file does not get stored on the machine upon commit. 228 | """ 229 | self._will_fake = True 230 | return self 231 | 232 | def restore(self) -> Self: 233 | """ 234 | Set the file to not fake the commit. 235 | This will ensure that the file gets stored on the machine upon commit. 236 | 237 | .. hint :: 238 | This is the default behavior. It's only useful to use this method if you have used :py:meth:`.fake`. 239 | """ 240 | self._will_fake = False 241 | return self 242 | 243 | def generate(self) -> str: 244 | """ 245 | Generate the file's content. 246 | 247 | Raises 248 | ------ 249 | :py:exc:`fabricius.exceptions.MissingRequiredValue` : 250 | If no content to the file were added. 251 | 252 | Returns 253 | ------- 254 | :py:class:`str` : 255 | The final content of the file. 256 | """ 257 | if not self.content: 258 | raise MissingRequiredValueError(self, "content") 259 | 260 | return self.renderer(self.data).render(self.content) 261 | 262 | def commit(self, *, overwrite: bool = False) -> FileCommitResult: 263 | """ 264 | Save the file to the disk. 265 | 266 | Parameters 267 | ---------- 268 | overwrite : :py:class:`bool` 269 | If a file exist at the given path, shall the overwrite parameter say if the file 270 | should be overwritten or not. Default to ``False``. 271 | 272 | Raises 273 | ------ 274 | :py:exc:`MissingRequiredValueError ` : 275 | If a required value was not set. (Content or destination) 276 | :py:exc:`fabricius.exceptions.AlreadyCommittedError` : 277 | If the file has already been saved to the disk. 278 | :py:exc:`FileExistsError` : 279 | If the file already exists on the disk and ``overwrite`` is set to ``False``. 280 | 281 | This is different from 282 | :py:exc:`AlreadyCommittedError ` 283 | because this indicates that the content of the file this generator was never actually 284 | saved. 285 | :py:exc:`OSError` : 286 | The file's name is not valid for the OS. 287 | 288 | Returns 289 | ------- 290 | :py:class:`fabricius.types.FileCommitResult` : 291 | A typed dict with information about the created file. 292 | """ 293 | if not self.destination: 294 | raise MissingRequiredValueError(self, "destination") 295 | if not self.content: 296 | raise MissingRequiredValueError(self, "content") 297 | if self.state == "persisted": 298 | raise AlreadyCommittedError(self.name) 299 | 300 | final_content = self.generate() 301 | 302 | destination = self.compute_destination() 303 | 304 | if destination.exists() and not overwrite: 305 | exception = FileExistsError(f"File '{self.name}' already exists.") 306 | exception.filename = self.name 307 | raise exception 308 | 309 | before_file_commit.send(self) 310 | 311 | try: 312 | if self._will_fake: 313 | self.state = "persisted" 314 | else: 315 | with contextlib.suppress(NotADirectoryError): 316 | destination.write_text(final_content) 317 | self.state = "persisted" 318 | except Exception as exception: 319 | on_file_commit_fail.send(self) 320 | 321 | commit = FileCommitResult( 322 | name=self.name, 323 | state=self.state, 324 | data=self.data, 325 | template_content=self.content, 326 | content=final_content, 327 | destination=self.destination.joinpath(self.name), 328 | fake=self._will_fake, 329 | ) 330 | 331 | after_file_commit.send(self, commit) 332 | return commit 333 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | arrow==1.2.3 ; python_version >= "3.10" and python_version < "4.0" \ 2 | --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ 3 | --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 4 | chevron==0.14.0 ; python_version >= "3.10" and python_version < "4.0" \ 5 | --hash=sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf \ 6 | --hash=sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443 7 | click==8.1.6 ; python_version >= "3.10" and python_version < "4.0" \ 8 | --hash=sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd \ 9 | --hash=sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5 10 | colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" \ 11 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 12 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 13 | commonmark==0.9.1 ; python_version >= "3.10" and python_version < "4.0" \ 14 | --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ 15 | --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 16 | inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0" \ 17 | --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ 18 | --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 19 | jinja2-time==0.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 20 | --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ 21 | --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa 22 | jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" \ 23 | --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ 24 | --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 25 | markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" \ 26 | --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ 27 | --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ 28 | --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ 29 | --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ 30 | --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ 31 | --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ 32 | --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ 33 | --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ 34 | --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ 35 | --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ 36 | --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ 37 | --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ 38 | --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ 39 | --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ 40 | --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ 41 | --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ 42 | --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ 43 | --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ 44 | --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ 45 | --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ 46 | --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ 47 | --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ 48 | --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ 49 | --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ 50 | --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ 51 | --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ 52 | --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ 53 | --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ 54 | --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ 55 | --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ 56 | --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ 57 | --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ 58 | --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ 59 | --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ 60 | --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ 61 | --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ 62 | --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ 63 | --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ 64 | --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ 65 | --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ 66 | --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ 67 | --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ 68 | --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ 69 | --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ 70 | --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ 71 | --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ 72 | --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ 73 | --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ 74 | --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ 75 | --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 76 | platformdirs==3.9.1 ; python_version >= "3.10" and python_version < "4.0" \ 77 | --hash=sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421 \ 78 | --hash=sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f 79 | pygments==2.15.1 ; python_version >= "3.10" and python_version < "4.0" \ 80 | --hash=sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c \ 81 | --hash=sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1 82 | python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" \ 83 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 84 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 85 | pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 86 | --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ 87 | --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ 88 | --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ 89 | --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ 90 | --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ 91 | --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ 92 | --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ 93 | --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ 94 | --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ 95 | --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ 96 | --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ 97 | --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ 98 | --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ 99 | --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ 100 | --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ 101 | --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ 102 | --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ 103 | --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ 104 | --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ 105 | --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ 106 | --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ 107 | --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ 108 | --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ 109 | --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ 110 | --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ 111 | --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ 112 | --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ 113 | --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ 114 | --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ 115 | --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ 116 | --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ 117 | --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ 118 | --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ 119 | --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ 120 | --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ 121 | --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ 122 | --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ 123 | --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ 124 | --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ 125 | --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f 126 | rich==12.6.0 ; python_version >= "3.10" and python_version < "4.0" \ 127 | --hash=sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e \ 128 | --hash=sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0 129 | six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ 130 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ 131 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 132 | slugify==0.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 133 | --hash=sha256:c5703cc11c1a6947536f3ce8bb306766b8bb5a84a53717f5a703ce0f18235e4c 134 | typing-extensions==4.7.1 ; python_version >= "3.10" and python_version < "4.0" \ 135 | --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ 136 | --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Fabricius 2 | 3 | First off, thanks for taking the time to contribute! ❤️ 4 | 5 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 6 | 7 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 8 | > 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's README 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | 14 | 15 | ## Table of Contents 16 | 17 | - [I Have a Question](#i-have-a-question) 18 | - [I Want To Contribute](#i-want-to-contribute) 19 | - [Reporting Bugs](#reporting-bugs) 20 | - [Suggesting Enhancements](#suggesting-enhancements) 21 | - [Your First Code Contribution](#your-first-code-contribution) 22 | - [Improving The Documentation](#improving-the-documentation) 23 | - [Styleguides](#styleguides) 24 | - [Commit Messages](#commit-messages) 25 | 26 | ## I Have a Question 27 | 28 | > If you want to ask a question, we assume that you have read the available [Documentation](https://fabricius.readthedocs.io). 29 | 30 | Before you ask a question, it is best to search for existing [Issues](https://github.com/Predeactor/Fabricius/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 31 | 32 | If you then still feel the need to ask a question and need clarification, we recommend the following: 33 | 34 | - Join the [Discord Server](https://discord.gg/aPVupKAxxP) 35 | - Open an [Issue](https://github.com/Predeactor/Fabricius/issues/new). 36 | - Provide as much context as you can about what you're running into. 37 | - Provide project and platform versions (Python, OS, etc.), depending on what seems relevant. 38 | 39 | We will then take care of the issue as soon as possible. 40 | 41 | ## I Want To Contribute 42 | 43 | > ### Legal Notice 44 | > 45 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. 46 | 47 | ### Reporting Bugs 48 | 49 | #### Before Submitting a Bug Report 50 | 51 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 52 | 53 | - Make sure that you are using the latest version. 54 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://fabricius.readthedocs.io). If you are looking for support, you might want to check [this section](#i-have-a-question)). 55 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Predeactor/Fabricius/issues?q=label%3Abug). 56 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 57 | - Collect information about the bug: 58 | - Stack trace (Traceback) 59 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 60 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 61 | - Possibly your input and the output 62 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 63 | 64 | #### How Do I Submit a Good Bug Report? 65 | 66 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to [pro.julien.mauroy@gmail.com](mailto:pro.julien.mauroy@gmail.com). You may add a PGP key to allow the messages to be sent encrypted as well. 67 | 68 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 69 | 70 | - Open an [Issue](https://github.com/Predeactor/Fabricius/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 71 | - Explain the behavior you would expect and the actual behavior. 72 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 73 | - Provide the information you collected in the previous section. 74 | 75 | Once it's filed: 76 | 77 | - The project team will label the issue accordingly. 78 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 79 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 80 | 81 | ### Suggesting Enhancements 82 | 83 | This section guides you through submitting an enhancement suggestion for Fabricius, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 84 | 85 | #### Before Submitting an Enhancement 86 | 87 | - Make sure that you are using the latest version. 88 | - Read the [documentation](https://fabricius.readthedocs.io) carefully and find out if the functionality is already covered, maybe by an individual configuration. 89 | - Perform a [search](https://github.com/Predeactor/Fabricius/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 90 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 91 | 92 | #### How Do I Submit a Good Enhancement Suggestion? 93 | 94 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/Predeactor/Fabricius/issues). 95 | 96 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 97 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 98 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 99 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 100 | - **Explain why this enhancement would be useful** to most Fabricius users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 101 | 102 | 103 | 104 | ### Your First Code Contribution 105 | 106 | 107 | We use [Poetry](https://python-poetry.org/) to manage our environment of development, it is extremely recommended you use this tool yourself when editing Fabricius source code. This guide will explain you how to configure, edit, push and request changes into Fabricius. 108 | 109 | #### Install Poetry 110 | 111 | To install Poetry, please follow the guide provided by the Poetry's documentation: 112 | 113 | #### Fork & Clone Fabricius repository 114 | 115 | PS: Do not forget to [install Git](https://docs.github.com/en/get-started/quickstart/set-up-git) ;) 116 | 117 | To fork the Fabricius's repository, you can follow the GitHub's documentation: 118 | 119 | This will create a repository of Fabricius with the exact same code in your account. 120 | 121 | After forking the repository, you can clone your repository locally, again, you can follow the GitHub's documentation: 122 | 123 | #### Create a virtual environment & install dependencies 124 | 125 | After installing Poetry, open a terminal into the cloned, you can run this command to install a virtual environment and install dependencies into it: `poetry install` 126 | After Poetry is done installing your virtual environment, you can jump into it by using `poetry shell` or run commands into it using `poetry run ` 127 | 128 | You now have to setup your IDE to make use of the create virtual environment. This guide does not cover this part as everyone may use a different IDE, we recommend you to read your IDE's documentation. 129 | 130 | - [Visual Studio Code Documentation](https://code.visualstudio.com/docs) 131 | - [JetBrain's PyCharm](https://www.jetbrains.com/help/pycharm/quick-start-guide.html) 132 | 133 | After then, you're ready to edit Fabricius safely! 134 | 135 | #### Commit your changes 136 | 137 | You have made your changes into Fabricius, that's awesome! Thank for your time and contribution, and it is now time to publish them to the world! 138 | 139 | Before you commit your changes, it would be preferable that you setup one of our tool, `pre-commit`, this tool will make sure you respect the project's rules so you can respect our consistency (As for example, we use Conventional Commit, and `pre-commit` check if your commit respect it). To install it, you need to run these 2 commands into your virtual environment: 140 | 141 | ```shell 142 | pre-commit install 143 | # If you wish to respect Conventional Commit 144 | pre-commit install -t commit-msg 145 | ``` 146 | 147 | You're all set! Now, when you commit your changes, `pre-commit` will run automatically. If you wish to run it manually, you just have to run the `pre-commit` command. 148 | 149 | If `pre-commit` fails, it has probably automatically added fixed files you can add to your changes. 150 | 151 | After committing your changes, you can push them to your repository, you're then able to create a pull request at Fabricius's repository, check out (Once again :)) the GitHub's documentation for how to do so: 152 | 153 | ## Styleguides 154 | 155 | Fabricius respect the Black formatting and isort. 156 | 157 | When you are committing changes, we'd like you to run `black` and `isort` to respect project's style. 158 | 159 | ### Commit Messages 160 | 161 | Fabricius respect the Conventional Commit 1.0.0. 162 | 163 | You can learn more about Conventional Commit here: 164 | You are free to use the Conventional Commit convention as long as you're not pushing directly to the repository of Fabricius. 165 | 166 | ## Attribution 167 | 168 | This guide was partially made by [contributing-gen](https://github.com/bttger/contributing-gen). Licensed under MIT license. 169 | -------------------------------------------------------------------------------- /requirements/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.13 ; python_version >= "3.10" and python_version < "4.0" \ 2 | --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \ 3 | --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2 4 | arrow==1.2.3 ; python_version >= "3.10" and python_version < "4.0" \ 5 | --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ 6 | --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 7 | babel==2.12.1 ; python_version >= "3.10" and python_version < "4.0" \ 8 | --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \ 9 | --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455 10 | beautifulsoup4==4.12.2 ; python_version >= "3.10" and python_version < "4.0" \ 11 | --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \ 12 | --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a 13 | certifi==2023.5.7 ; python_version >= "3.10" and python_version < "4.0" \ 14 | --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ 15 | --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 16 | charset-normalizer==3.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 17 | --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \ 18 | --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \ 19 | --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \ 20 | --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \ 21 | --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \ 22 | --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \ 23 | --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \ 24 | --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \ 25 | --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \ 26 | --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \ 27 | --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \ 28 | --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \ 29 | --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \ 30 | --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \ 31 | --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \ 32 | --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \ 33 | --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \ 34 | --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \ 35 | --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \ 36 | --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \ 37 | --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \ 38 | --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \ 39 | --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \ 40 | --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \ 41 | --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \ 42 | --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \ 43 | --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \ 44 | --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \ 45 | --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \ 46 | --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \ 47 | --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \ 48 | --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \ 49 | --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \ 50 | --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \ 51 | --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \ 52 | --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \ 53 | --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \ 54 | --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \ 55 | --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \ 56 | --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \ 57 | --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \ 58 | --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \ 59 | --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \ 60 | --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \ 61 | --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \ 62 | --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \ 63 | --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \ 64 | --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \ 65 | --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \ 66 | --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \ 67 | --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \ 68 | --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \ 69 | --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \ 70 | --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \ 71 | --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \ 72 | --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \ 73 | --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \ 74 | --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \ 75 | --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \ 76 | --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \ 77 | --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \ 78 | --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \ 79 | --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \ 80 | --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \ 81 | --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \ 82 | --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \ 83 | --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \ 84 | --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \ 85 | --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \ 86 | --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \ 87 | --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \ 88 | --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \ 89 | --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \ 90 | --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \ 91 | --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa 92 | chevron==0.14.0 ; python_version >= "3.10" and python_version < "4.0" \ 93 | --hash=sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf \ 94 | --hash=sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443 95 | click==8.1.6 ; python_version >= "3.10" and python_version < "4.0" \ 96 | --hash=sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd \ 97 | --hash=sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5 98 | colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" \ 99 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 100 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 101 | commonmark==0.9.1 ; python_version >= "3.10" and python_version < "4.0" \ 102 | --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ 103 | --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 104 | docutils==0.19 ; python_version >= "3.10" and python_version < "4.0" \ 105 | --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ 106 | --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc 107 | furo==2022.12.7 ; python_version >= "3.10" and python_version < "4.0" \ 108 | --hash=sha256:7cb76c12a25ef65db85ab0743df907573d03027a33631f17d267e598ebb191f7 \ 109 | --hash=sha256:d8008f8efbe7587a97ba533c8b2df1f9c21ee9b3e5cad0d27f61193d38b1a986 110 | idna==3.4 ; python_version >= "3.10" and python_version < "4.0" \ 111 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ 112 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 113 | imagesize==1.4.1 ; python_version >= "3.10" and python_version < "4.0" \ 114 | --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ 115 | --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a 116 | inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0" \ 117 | --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ 118 | --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 119 | jinja2-time==0.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 120 | --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ 121 | --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa 122 | jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" \ 123 | --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ 124 | --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 125 | livereload==2.6.3 ; python_version >= "3.10" and python_version < "4.0" \ 126 | --hash=sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869 \ 127 | --hash=sha256:ad4ac6f53b2d62bb6ce1a5e6e96f1f00976a32348afedcb4b6d68df2a1d346e4 128 | markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" \ 129 | --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ 130 | --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ 131 | --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ 132 | --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ 133 | --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ 134 | --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ 135 | --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ 136 | --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ 137 | --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ 138 | --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ 139 | --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ 140 | --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ 141 | --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ 142 | --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ 143 | --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ 144 | --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ 145 | --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ 146 | --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ 147 | --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ 148 | --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ 149 | --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ 150 | --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ 151 | --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ 152 | --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ 153 | --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ 154 | --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ 155 | --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ 156 | --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ 157 | --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ 158 | --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ 159 | --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ 160 | --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ 161 | --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ 162 | --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ 163 | --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ 164 | --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ 165 | --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ 166 | --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ 167 | --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ 168 | --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ 169 | --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ 170 | --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ 171 | --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ 172 | --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ 173 | --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ 174 | --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ 175 | --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ 176 | --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ 177 | --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ 178 | --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 179 | packaging==23.1 ; python_version >= "3.10" and python_version < "4.0" \ 180 | --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ 181 | --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f 182 | platformdirs==3.9.1 ; python_version >= "3.10" and python_version < "4.0" \ 183 | --hash=sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421 \ 184 | --hash=sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f 185 | pygments==2.15.1 ; python_version >= "3.10" and python_version < "4.0" \ 186 | --hash=sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c \ 187 | --hash=sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1 188 | python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" \ 189 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 190 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 191 | pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 192 | --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ 193 | --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ 194 | --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ 195 | --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ 196 | --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ 197 | --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ 198 | --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ 199 | --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ 200 | --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ 201 | --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ 202 | --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ 203 | --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ 204 | --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ 205 | --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ 206 | --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ 207 | --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ 208 | --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ 209 | --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ 210 | --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ 211 | --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ 212 | --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ 213 | --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ 214 | --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ 215 | --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ 216 | --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ 217 | --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ 218 | --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ 219 | --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ 220 | --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ 221 | --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ 222 | --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ 223 | --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ 224 | --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ 225 | --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ 226 | --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ 227 | --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ 228 | --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ 229 | --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ 230 | --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ 231 | --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f 232 | requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0" \ 233 | --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ 234 | --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 235 | rich==12.6.0 ; python_version >= "3.10" and python_version < "4.0" \ 236 | --hash=sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e \ 237 | --hash=sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0 238 | six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ 239 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ 240 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 241 | slugify==0.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 242 | --hash=sha256:c5703cc11c1a6947536f3ce8bb306766b8bb5a84a53717f5a703ce0f18235e4c 243 | snowballstemmer==2.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 244 | --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ 245 | --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a 246 | soupsieve==2.4.1 ; python_version >= "3.10" and python_version < "4.0" \ 247 | --hash=sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8 \ 248 | --hash=sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea 249 | sphinx-autobuild==2021.3.14 ; python_version >= "3.10" and python_version < "4.0" \ 250 | --hash=sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac \ 251 | --hash=sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05 252 | sphinx-basic-ng==1.0.0b2 ; python_version >= "3.10" and python_version < "4.0" \ 253 | --hash=sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9 \ 254 | --hash=sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b 255 | sphinx==5.3.0 ; python_version >= "3.10" and python_version < "4.0" \ 256 | --hash=sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d \ 257 | --hash=sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5 258 | sphinxcontrib-applehelp==1.0.4 ; python_version >= "3.10" and python_version < "4.0" \ 259 | --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \ 260 | --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e 261 | sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.10" and python_version < "4.0" \ 262 | --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ 263 | --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 264 | sphinxcontrib-htmlhelp==2.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 265 | --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \ 266 | --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903 267 | sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 268 | --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ 269 | --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 270 | sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.10" and python_version < "4.0" \ 271 | --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ 272 | --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 273 | sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.10" and python_version < "4.0" \ 274 | --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ 275 | --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 276 | tornado==6.3.2 ; python_version >= "3.10" and python_version < "4.0" \ 277 | --hash=sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4 \ 278 | --hash=sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf \ 279 | --hash=sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d \ 280 | --hash=sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba \ 281 | --hash=sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe \ 282 | --hash=sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411 \ 283 | --hash=sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2 \ 284 | --hash=sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0 \ 285 | --hash=sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c \ 286 | --hash=sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f \ 287 | --hash=sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829 288 | typing-extensions==4.7.1 ; python_version >= "3.10" and python_version < "4.0" \ 289 | --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ 290 | --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 291 | urllib3==2.0.4 ; python_version >= "3.10" and python_version < "4.0" \ 292 | --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \ 293 | --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4 294 | -------------------------------------------------------------------------------- /requirements/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.13 ; python_version >= "3.10" and python_version < "4.0" \ 2 | --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \ 3 | --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2 4 | appdirs==1.4.4 ; python_version >= "3.10" and python_version < "4.0" \ 5 | --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ 6 | --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 7 | appnope==0.1.3 ; python_version >= "3.10" and python_version < "4.0" and (platform_system == "Darwin" or sys_platform == "darwin") \ 8 | --hash=sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24 \ 9 | --hash=sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e 10 | arrow==1.2.3 ; python_version >= "3.10" and python_version < "4.0" \ 11 | --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ 12 | --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 13 | asttokens==2.2.1 ; python_version >= "3.10" and python_version < "4.0" \ 14 | --hash=sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3 \ 15 | --hash=sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c 16 | attrs==23.1.0 ; python_version >= "3.10" and python_version < "4" \ 17 | --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ 18 | --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 19 | babel==2.12.1 ; python_version >= "3.10" and python_version < "4.0" \ 20 | --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \ 21 | --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455 22 | backcall==0.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 23 | --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \ 24 | --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255 25 | black==23.7.0 ; python_version >= "3.10" and python_version < "4.0" \ 26 | --hash=sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3 \ 27 | --hash=sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb \ 28 | --hash=sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087 \ 29 | --hash=sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320 \ 30 | --hash=sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6 \ 31 | --hash=sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3 \ 32 | --hash=sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc \ 33 | --hash=sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f \ 34 | --hash=sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587 \ 35 | --hash=sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91 \ 36 | --hash=sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a \ 37 | --hash=sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad \ 38 | --hash=sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926 \ 39 | --hash=sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9 \ 40 | --hash=sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be \ 41 | --hash=sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd \ 42 | --hash=sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96 \ 43 | --hash=sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491 \ 44 | --hash=sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2 \ 45 | --hash=sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a \ 46 | --hash=sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f \ 47 | --hash=sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995 48 | cachetools==5.3.1 ; python_version >= "3.10" and python_version < "4.0" \ 49 | --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \ 50 | --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b 51 | cattrs==23.1.2 ; python_version >= "3.10" and python_version < "4" \ 52 | --hash=sha256:b2bb14311ac17bed0d58785e5a60f022e5431aca3932e3fc5cc8ed8639de50a4 \ 53 | --hash=sha256:db1c821b8c537382b2c7c66678c3790091ca0275ac486c76f3c8f3920e83c657 54 | certifi==2023.5.7 ; python_version >= "3.10" and python_version < "4.0" \ 55 | --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ 56 | --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 57 | cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0" and implementation_name == "pypy" \ 58 | --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \ 59 | --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \ 60 | --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \ 61 | --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \ 62 | --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \ 63 | --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \ 64 | --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \ 65 | --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \ 66 | --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \ 67 | --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \ 68 | --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \ 69 | --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \ 70 | --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \ 71 | --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \ 72 | --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \ 73 | --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \ 74 | --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \ 75 | --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \ 76 | --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \ 77 | --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \ 78 | --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \ 79 | --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \ 80 | --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \ 81 | --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \ 82 | --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \ 83 | --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \ 84 | --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \ 85 | --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \ 86 | --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \ 87 | --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \ 88 | --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \ 89 | --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \ 90 | --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \ 91 | --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \ 92 | --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \ 93 | --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \ 94 | --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \ 95 | --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \ 96 | --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \ 97 | --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \ 98 | --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \ 99 | --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \ 100 | --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \ 101 | --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \ 102 | --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \ 103 | --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \ 104 | --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \ 105 | --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \ 106 | --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \ 107 | --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \ 108 | --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \ 109 | --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \ 110 | --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \ 111 | --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \ 112 | --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \ 113 | --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \ 114 | --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \ 115 | --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \ 116 | --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \ 117 | --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \ 118 | --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \ 119 | --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \ 120 | --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \ 121 | --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0 122 | cfgv==3.3.1 ; python_version >= "3.10" and python_version < "4.0" \ 123 | --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \ 124 | --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736 125 | chardet==5.1.0 ; python_version >= "3.10" and python_version < "4.0" \ 126 | --hash=sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5 \ 127 | --hash=sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9 128 | charset-normalizer==3.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 129 | --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \ 130 | --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \ 131 | --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \ 132 | --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \ 133 | --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \ 134 | --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \ 135 | --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \ 136 | --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \ 137 | --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \ 138 | --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \ 139 | --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \ 140 | --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \ 141 | --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \ 142 | --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \ 143 | --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \ 144 | --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \ 145 | --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \ 146 | --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \ 147 | --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \ 148 | --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \ 149 | --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \ 150 | --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \ 151 | --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \ 152 | --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \ 153 | --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \ 154 | --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \ 155 | --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \ 156 | --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \ 157 | --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \ 158 | --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \ 159 | --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \ 160 | --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \ 161 | --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \ 162 | --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \ 163 | --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \ 164 | --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \ 165 | --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \ 166 | --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \ 167 | --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \ 168 | --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \ 169 | --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \ 170 | --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \ 171 | --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \ 172 | --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \ 173 | --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \ 174 | --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \ 175 | --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \ 176 | --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \ 177 | --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \ 178 | --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \ 179 | --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \ 180 | --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \ 181 | --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \ 182 | --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \ 183 | --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \ 184 | --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \ 185 | --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \ 186 | --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \ 187 | --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \ 188 | --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \ 189 | --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \ 190 | --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \ 191 | --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \ 192 | --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \ 193 | --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \ 194 | --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \ 195 | --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \ 196 | --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \ 197 | --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \ 198 | --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \ 199 | --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \ 200 | --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \ 201 | --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \ 202 | --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \ 203 | --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa 204 | chevron==0.14.0 ; python_version >= "3.10" and python_version < "4.0" \ 205 | --hash=sha256:87613aafdf6d77b6a90ff073165a61ae5086e21ad49057aa0e53681601800ebf \ 206 | --hash=sha256:fbf996a709f8da2e745ef763f482ce2d311aa817d287593a5b990d6d6e4f0443 207 | click==8.1.6 ; python_version >= "3.10" and python_version < "4.0" \ 208 | --hash=sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd \ 209 | --hash=sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5 210 | colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" \ 211 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 212 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 213 | comm==0.1.3 ; python_version >= "3.10" and python_version < "4.0" \ 214 | --hash=sha256:16613c6211e20223f215fc6d3b266a247b6e2641bf4e0a3ad34cb1aff2aa3f37 \ 215 | --hash=sha256:a61efa9daffcfbe66fd643ba966f846a624e4e6d6767eda9cf6e993aadaab93e 216 | commonmark==0.9.1 ; python_version >= "3.10" and python_version < "4.0" \ 217 | --hash=sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60 \ 218 | --hash=sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9 219 | debugpy==1.6.7 ; python_version >= "3.10" and python_version < "4.0" \ 220 | --hash=sha256:0679b7e1e3523bd7d7869447ec67b59728675aadfc038550a63a362b63029d2c \ 221 | --hash=sha256:279d64c408c60431c8ee832dfd9ace7c396984fd7341fa3116aee414e7dcd88d \ 222 | --hash=sha256:33edb4afa85c098c24cc361d72ba7c21bb92f501104514d4ffec1fb36e09c01a \ 223 | --hash=sha256:38ed626353e7c63f4b11efad659be04c23de2b0d15efff77b60e4740ea685d07 \ 224 | --hash=sha256:5224eabbbeddcf1943d4e2821876f3e5d7d383f27390b82da5d9558fd4eb30a9 \ 225 | --hash=sha256:53f7a456bc50706a0eaabecf2d3ce44c4d5010e46dfc65b6b81a518b42866267 \ 226 | --hash=sha256:9cd10cf338e0907fdcf9eac9087faa30f150ef5445af5a545d307055141dd7a4 \ 227 | --hash=sha256:aaf6da50377ff4056c8ed470da24632b42e4087bc826845daad7af211e00faad \ 228 | --hash=sha256:b3e7ac809b991006ad7f857f016fa92014445085711ef111fdc3f74f66144096 \ 229 | --hash=sha256:bae1123dff5bfe548ba1683eb972329ba6d646c3a80e6b4c06cd1b1dd0205e9b \ 230 | --hash=sha256:c0ff93ae90a03b06d85b2c529eca51ab15457868a377c4cc40a23ab0e4e552a3 \ 231 | --hash=sha256:c4c2f0810fa25323abfdfa36cbbbb24e5c3b1a42cb762782de64439c575d67f2 \ 232 | --hash=sha256:d71b31117779d9a90b745720c0eab54ae1da76d5b38c8026c654f4a066b0130a \ 233 | --hash=sha256:dbe04e7568aa69361a5b4c47b4493d5680bfa3a911d1e105fbea1b1f23f3eb45 \ 234 | --hash=sha256:de86029696e1b3b4d0d49076b9eba606c226e33ae312a57a46dca14ff370894d \ 235 | --hash=sha256:e3876611d114a18aafef6383695dfc3f1217c98a9168c1aaf1a02b01ec7d8d1e \ 236 | --hash=sha256:ed6d5413474e209ba50b1a75b2d9eecf64d41e6e4501977991cdc755dc83ab0f \ 237 | --hash=sha256:f90a2d4ad9a035cee7331c06a4cf2245e38bd7c89554fe3b616d90ab8aab89cc 238 | decorator==5.1.1 ; python_version >= "3.10" and python_version < "4.0" \ 239 | --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ 240 | --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 241 | distlib==0.3.7 ; python_version >= "3.10" and python_version < "4.0" \ 242 | --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \ 243 | --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8 244 | docutils==0.19 ; python_version >= "3.10" and python_version < "4.0" \ 245 | --hash=sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6 \ 246 | --hash=sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc 247 | esbonio==0.16.1 ; python_version >= "3.10" and python_version < "4.0" \ 248 | --hash=sha256:cd5ed65666d7566a38b04cac01dd55b7d74147a016d1b2ac0a0f971a8cd510cc \ 249 | --hash=sha256:d5d57c29a793de9b9b1894f9bffdd3bd25a79d430459e3e597424f52e8d02320 250 | exceptiongroup==1.1.2 ; python_version >= "3.10" and python_version < "3.11" \ 251 | --hash=sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5 \ 252 | --hash=sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f 253 | executing==1.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 254 | --hash=sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc \ 255 | --hash=sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107 256 | filelock==3.12.2 ; python_version >= "3.10" and python_version < "4.0" \ 257 | --hash=sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81 \ 258 | --hash=sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec 259 | identify==2.5.25 ; python_version >= "3.10" and python_version < "4.0" \ 260 | --hash=sha256:9df2489842707d431b38ce3410ef8df40da5b10a3e28a3fcac1a42523e956409 \ 261 | --hash=sha256:db4de0e758c0db8f81996816cd2f3f2f8c5c8d49a7fd02f3b4109aac6fd80e29 262 | idna==3.4 ; python_version >= "3.10" and python_version < "4.0" \ 263 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ 264 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 265 | imagesize==1.4.1 ; python_version >= "3.10" and python_version < "4.0" \ 266 | --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ 267 | --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a 268 | inflection==0.5.1 ; python_version >= "3.10" and python_version < "4.0" \ 269 | --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ 270 | --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 271 | iniconfig==2.0.0 ; python_version >= "3.10" and python_version < "4.0" \ 272 | --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ 273 | --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 274 | ipykernel==6.24.0 ; python_version >= "3.10" and python_version < "4.0" \ 275 | --hash=sha256:29cea0a716b1176d002a61d0b0c851f34536495bc4ef7dd0222c88b41b816123 \ 276 | --hash=sha256:2f5fffc7ad8f1fd5aadb4e171ba9129d9668dbafa374732cf9511ada52d6547f 277 | ipython==8.14.0 ; python_version >= "3.10" and python_version < "4.0" \ 278 | --hash=sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1 \ 279 | --hash=sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf 280 | isort==5.12.0 ; python_version >= "3.10" and python_version < "4.0" \ 281 | --hash=sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504 \ 282 | --hash=sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6 283 | jedi==0.18.2 ; python_version >= "3.10" and python_version < "4.0" \ 284 | --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ 285 | --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 286 | jinja2-time==0.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 287 | --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ 288 | --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa 289 | jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0" \ 290 | --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ 291 | --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 292 | jupyter-client==8.3.0 ; python_version >= "3.10" and python_version < "4.0" \ 293 | --hash=sha256:3af69921fe99617be1670399a0b857ad67275eefcfa291e2c81a160b7b650f5f \ 294 | --hash=sha256:7441af0c0672edc5d28035e92ba5e32fadcfa8a4e608a434c228836a89df6158 295 | jupyter-core==5.3.1 ; python_version >= "3.10" and python_version < "4.0" \ 296 | --hash=sha256:5ba5c7938a7f97a6b0481463f7ff0dbac7c15ba48cf46fa4035ca6e838aa1aba \ 297 | --hash=sha256:ae9036db959a71ec1cac33081eeb040a79e681f08ab68b0883e9a676c7a90dce 298 | lsprotocol==2023.0.0a2 ; python_version >= "3.10" and python_version < "4" \ 299 | --hash=sha256:80aae7e39171b49025876a524937c10be2eb986f4be700ca22ee7d186b8488aa \ 300 | --hash=sha256:c4f2f77712b50d065b17f9b50d2b88c480dc2ce4bbaa56eea8269dbf54bc9701 301 | markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0" \ 302 | --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ 303 | --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ 304 | --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ 305 | --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ 306 | --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ 307 | --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ 308 | --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ 309 | --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ 310 | --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ 311 | --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ 312 | --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ 313 | --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ 314 | --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ 315 | --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ 316 | --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ 317 | --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ 318 | --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ 319 | --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ 320 | --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ 321 | --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ 322 | --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ 323 | --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ 324 | --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ 325 | --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ 326 | --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ 327 | --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ 328 | --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ 329 | --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ 330 | --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ 331 | --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ 332 | --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ 333 | --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ 334 | --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ 335 | --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ 336 | --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ 337 | --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ 338 | --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ 339 | --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ 340 | --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ 341 | --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ 342 | --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ 343 | --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ 344 | --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ 345 | --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ 346 | --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ 347 | --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ 348 | --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ 349 | --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ 350 | --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ 351 | --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 352 | matplotlib-inline==0.1.6 ; python_version >= "3.10" and python_version < "4.0" \ 353 | --hash=sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311 \ 354 | --hash=sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304 355 | mypy-extensions==1.0.0 ; python_version >= "3.10" and python_version < "4.0" \ 356 | --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \ 357 | --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782 358 | mypy==1.4.1 ; python_version >= "3.10" and python_version < "4.0" \ 359 | --hash=sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042 \ 360 | --hash=sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd \ 361 | --hash=sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2 \ 362 | --hash=sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01 \ 363 | --hash=sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7 \ 364 | --hash=sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3 \ 365 | --hash=sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816 \ 366 | --hash=sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3 \ 367 | --hash=sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc \ 368 | --hash=sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4 \ 369 | --hash=sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b \ 370 | --hash=sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8 \ 371 | --hash=sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c \ 372 | --hash=sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462 \ 373 | --hash=sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7 \ 374 | --hash=sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc \ 375 | --hash=sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258 \ 376 | --hash=sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b \ 377 | --hash=sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9 \ 378 | --hash=sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6 \ 379 | --hash=sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f \ 380 | --hash=sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1 \ 381 | --hash=sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828 \ 382 | --hash=sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878 \ 383 | --hash=sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f \ 384 | --hash=sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b 385 | nest-asyncio==1.5.6 ; python_version >= "3.10" and python_version < "4.0" \ 386 | --hash=sha256:b9a953fb40dceaa587d109609098db21900182b16440652454a146cffb06e8b8 \ 387 | --hash=sha256:d267cc1ff794403f7df692964d1d2a3fa9418ffea2a3f6859a439ff482fef290 388 | nodeenv==1.8.0 ; python_version >= "3.10" and python_version < "4.0" \ 389 | --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \ 390 | --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec 391 | packaging==23.1 ; python_version >= "3.10" and python_version < "4.0" \ 392 | --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ 393 | --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f 394 | parso==0.8.3 ; python_version >= "3.10" and python_version < "4.0" \ 395 | --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ 396 | --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 397 | pastel==0.2.1 ; python_version >= "3.10" and python_version < "4.0" \ 398 | --hash=sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364 \ 399 | --hash=sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d 400 | pathspec==0.11.1 ; python_version >= "3.10" and python_version < "4.0" \ 401 | --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \ 402 | --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293 403 | pexpect==4.8.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform != "win32" \ 404 | --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \ 405 | --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c 406 | pickleshare==0.7.5 ; python_version >= "3.10" and python_version < "4.0" \ 407 | --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \ 408 | --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 409 | platformdirs==3.9.1 ; python_version >= "3.10" and python_version < "4.0" \ 410 | --hash=sha256:1b42b450ad933e981d56e59f1b97495428c9bd60698baab9f3eb3d00d5822421 \ 411 | --hash=sha256:ad8291ae0ae5072f66c16945166cb11c63394c7a3ad1b1bc9828ca3162da8c2f 412 | pluggy==1.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 413 | --hash=sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849 \ 414 | --hash=sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3 415 | poethepoet==0.20.0 ; python_version >= "3.10" and python_version < "4.0" \ 416 | --hash=sha256:ca5a2a955f52dfb0a53fad3c989ef0b69ce3d5ec0f6bfa9b1da1f9e32d262e20 \ 417 | --hash=sha256:cb37be15f3895ccc65ddf188c2e3d8fb79e26cc9d469a6098cb1c6f994659f6f 418 | pre-commit==2.21.0 ; python_version >= "3.10" and python_version < "4.0" \ 419 | --hash=sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658 \ 420 | --hash=sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad 421 | prompt-toolkit==3.0.39 ; python_version >= "3.10" and python_version < "4.0" \ 422 | --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \ 423 | --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88 424 | psutil==5.9.5 ; python_version >= "3.10" and python_version < "4.0" \ 425 | --hash=sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d \ 426 | --hash=sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217 \ 427 | --hash=sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4 \ 428 | --hash=sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c \ 429 | --hash=sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f \ 430 | --hash=sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da \ 431 | --hash=sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4 \ 432 | --hash=sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42 \ 433 | --hash=sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5 \ 434 | --hash=sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4 \ 435 | --hash=sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9 \ 436 | --hash=sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f \ 437 | --hash=sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30 \ 438 | --hash=sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48 439 | ptyprocess==0.7.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform != "win32" \ 440 | --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ 441 | --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 442 | pure-eval==0.2.2 ; python_version >= "3.10" and python_version < "4.0" \ 443 | --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \ 444 | --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3 445 | pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0" and implementation_name == "pypy" \ 446 | --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ 447 | --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 448 | pygls==1.0.2 ; python_version >= "3.10" and python_version < "4" \ 449 | --hash=sha256:6d278d29fa6559b0f7a448263c85cb64ec6e9369548b02f1a7944060848b21f9 \ 450 | --hash=sha256:888ed63d1f650b4fc64d603d73d37545386ec533c0caac921aed80f80ea946a4 451 | pygments==2.15.1 ; python_version >= "3.10" and python_version < "4.0" \ 452 | --hash=sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c \ 453 | --hash=sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1 454 | pyproject-api==1.5.3 ; python_version >= "3.10" and python_version < "4.0" \ 455 | --hash=sha256:14cf09828670c7b08842249c1f28c8ee6581b872e893f81b62d5465bec41502f \ 456 | --hash=sha256:ffb5b2d7cad43f5b2688ab490de7c4d3f6f15e0b819cb588c4b771567c9729eb 457 | pyspellchecker==0.7.2 ; python_version >= "3.10" and python_version < "4.0" \ 458 | --hash=sha256:b5ef23437702b8d03626f814b9646779b572d378b325ad252d8a8e616b3d76db \ 459 | --hash=sha256:bc51ffb2c18ba26eaa1340756ebf96d0d886ed6a31d6f8e7a0094ad49d24550a 460 | pytest==7.4.0 ; python_version >= "3.10" and python_version < "4.0" \ 461 | --hash=sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32 \ 462 | --hash=sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a 463 | python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" \ 464 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 465 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 466 | pywin32==306 ; sys_platform == "win32" and platform_python_implementation != "PyPy" and python_version >= "3.10" and python_version < "4.0" \ 467 | --hash=sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d \ 468 | --hash=sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65 \ 469 | --hash=sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e \ 470 | --hash=sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b \ 471 | --hash=sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4 \ 472 | --hash=sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040 \ 473 | --hash=sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a \ 474 | --hash=sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36 \ 475 | --hash=sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8 \ 476 | --hash=sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e \ 477 | --hash=sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802 \ 478 | --hash=sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a \ 479 | --hash=sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407 \ 480 | --hash=sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0 481 | pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 482 | --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ 483 | --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ 484 | --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ 485 | --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ 486 | --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ 487 | --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ 488 | --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ 489 | --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ 490 | --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ 491 | --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ 492 | --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ 493 | --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ 494 | --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ 495 | --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ 496 | --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ 497 | --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ 498 | --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ 499 | --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ 500 | --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ 501 | --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ 502 | --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ 503 | --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ 504 | --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ 505 | --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ 506 | --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ 507 | --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ 508 | --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ 509 | --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ 510 | --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ 511 | --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ 512 | --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ 513 | --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ 514 | --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ 515 | --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ 516 | --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ 517 | --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ 518 | --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ 519 | --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ 520 | --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ 521 | --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f 522 | pyzmq==25.1.0 ; python_version >= "3.10" and python_version < "4.0" \ 523 | --hash=sha256:01f06f33e12497dca86353c354461f75275a5ad9eaea181ac0dc1662da8074fa \ 524 | --hash=sha256:0b6b42f7055bbc562f63f3df3b63e3dd1ebe9727ff0f124c3aa7bcea7b3a00f9 \ 525 | --hash=sha256:0c4fc2741e0513b5d5a12fe200d6785bbcc621f6f2278893a9ca7bed7f2efb7d \ 526 | --hash=sha256:108c96ebbd573d929740d66e4c3d1bdf31d5cde003b8dc7811a3c8c5b0fc173b \ 527 | --hash=sha256:13bbe36da3f8aaf2b7ec12696253c0bf6ffe05f4507985a8844a1081db6ec22d \ 528 | --hash=sha256:154bddda2a351161474b36dba03bf1463377ec226a13458725183e508840df89 \ 529 | --hash=sha256:19d0383b1f18411d137d891cab567de9afa609b214de68b86e20173dc624c101 \ 530 | --hash=sha256:1a6169e69034eaa06823da6a93a7739ff38716142b3596c180363dee729d713d \ 531 | --hash=sha256:1fc56a0221bdf67cfa94ef2d6ce5513a3d209c3dfd21fed4d4e87eca1822e3a3 \ 532 | --hash=sha256:2a21fec5c3cea45421a19ccbe6250c82f97af4175bc09de4d6dd78fb0cb4c200 \ 533 | --hash=sha256:2b15247c49d8cbea695b321ae5478d47cffd496a2ec5ef47131a9e79ddd7e46c \ 534 | --hash=sha256:2f5efcc29056dfe95e9c9db0dfbb12b62db9c4ad302f812931b6d21dd04a9119 \ 535 | --hash=sha256:2f666ae327a6899ff560d741681fdcdf4506f990595201ed39b44278c471ad98 \ 536 | --hash=sha256:332616f95eb400492103ab9d542b69d5f0ff628b23129a4bc0a2fd48da6e4e0b \ 537 | --hash=sha256:33d5c8391a34d56224bccf74f458d82fc6e24b3213fc68165c98b708c7a69325 \ 538 | --hash=sha256:3575699d7fd7c9b2108bc1c6128641a9a825a58577775ada26c02eb29e09c517 \ 539 | --hash=sha256:3830be8826639d801de9053cf86350ed6742c4321ba4236e4b5568528d7bfed7 \ 540 | --hash=sha256:3a522510e3434e12aff80187144c6df556bb06fe6b9d01b2ecfbd2b5bfa5c60c \ 541 | --hash=sha256:3bed53f7218490c68f0e82a29c92335daa9606216e51c64f37b48eb78f1281f4 \ 542 | --hash=sha256:414b8beec76521358b49170db7b9967d6974bdfc3297f47f7d23edec37329b00 \ 543 | --hash=sha256:442d3efc77ca4d35bee3547a8e08e8d4bb88dadb54a8377014938ba98d2e074a \ 544 | --hash=sha256:47b915ba666c51391836d7ed9a745926b22c434efa76c119f77bcffa64d2c50c \ 545 | --hash=sha256:48e5e59e77c1a83162ab3c163fc01cd2eebc5b34560341a67421b09be0891287 \ 546 | --hash=sha256:4a82faae00d1eed4809c2f18b37f15ce39a10a1c58fe48b60ad02875d6e13d80 \ 547 | --hash=sha256:4a983c8694667fd76d793ada77fd36c8317e76aa66eec75be2653cef2ea72883 \ 548 | --hash=sha256:4c2fc7aad520a97d64ffc98190fce6b64152bde57a10c704b337082679e74f67 \ 549 | --hash=sha256:4cb27ef9d3bdc0c195b2dc54fcb8720e18b741624686a81942e14c8b67cc61a6 \ 550 | --hash=sha256:4d67609b37204acad3d566bb7391e0ecc25ef8bae22ff72ebe2ad7ffb7847158 \ 551 | --hash=sha256:5482f08d2c3c42b920e8771ae8932fbaa0a67dff925fc476996ddd8155a170f3 \ 552 | --hash=sha256:5489738a692bc7ee9a0a7765979c8a572520d616d12d949eaffc6e061b82b4d1 \ 553 | --hash=sha256:5693dcc4f163481cf79e98cf2d7995c60e43809e325b77a7748d8024b1b7bcba \ 554 | --hash=sha256:58416db767787aedbfd57116714aad6c9ce57215ffa1c3758a52403f7c68cff5 \ 555 | --hash=sha256:5873d6a60b778848ce23b6c0ac26c39e48969823882f607516b91fb323ce80e5 \ 556 | --hash=sha256:5af31493663cf76dd36b00dafbc839e83bbca8a0662931e11816d75f36155897 \ 557 | --hash=sha256:5e7fbcafa3ea16d1de1f213c226005fea21ee16ed56134b75b2dede5a2129e62 \ 558 | --hash=sha256:65346f507a815a731092421d0d7d60ed551a80d9b75e8b684307d435a5597425 \ 559 | --hash=sha256:6581e886aec3135964a302a0f5eb68f964869b9efd1dbafdebceaaf2934f8a68 \ 560 | --hash=sha256:69511d604368f3dc58d4be1b0bad99b61ee92b44afe1cd9b7bd8c5e34ea8248a \ 561 | --hash=sha256:7018289b402ebf2b2c06992813523de61d4ce17bd514c4339d8f27a6f6809492 \ 562 | --hash=sha256:71c7b5896e40720d30cd77a81e62b433b981005bbff0cb2f739e0f8d059b5d99 \ 563 | --hash=sha256:75217e83faea9edbc29516fc90c817bc40c6b21a5771ecb53e868e45594826b0 \ 564 | --hash=sha256:7e23a8c3b6c06de40bdb9e06288180d630b562db8ac199e8cc535af81f90e64b \ 565 | --hash=sha256:80c41023465d36280e801564a69cbfce8ae85ff79b080e1913f6e90481fb8957 \ 566 | --hash=sha256:831ba20b660b39e39e5ac8603e8193f8fce1ee03a42c84ade89c36a251449d80 \ 567 | --hash=sha256:851fb2fe14036cfc1960d806628b80276af5424db09fe5c91c726890c8e6d943 \ 568 | --hash=sha256:8751f9c1442624da391bbd92bd4b072def6d7702a9390e4479f45c182392ff78 \ 569 | --hash=sha256:8b45d722046fea5a5694cba5d86f21f78f0052b40a4bbbbf60128ac55bfcc7b6 \ 570 | --hash=sha256:8b697774ea8273e3c0460cf0bba16cd85ca6c46dfe8b303211816d68c492e132 \ 571 | --hash=sha256:90146ab578931e0e2826ee39d0c948d0ea72734378f1898939d18bc9c823fcf9 \ 572 | --hash=sha256:9301cf1d7fc1ddf668d0abbe3e227fc9ab15bc036a31c247276012abb921b5ff \ 573 | --hash=sha256:95bd3a998d8c68b76679f6b18f520904af5204f089beebb7b0301d97704634dd \ 574 | --hash=sha256:968b0c737797c1809ec602e082cb63e9824ff2329275336bb88bd71591e94a90 \ 575 | --hash=sha256:97d984b1b2f574bc1bb58296d3c0b64b10e95e7026f8716ed6c0b86d4679843f \ 576 | --hash=sha256:9e68ae9864d260b18f311b68d29134d8776d82e7f5d75ce898b40a88df9db30f \ 577 | --hash=sha256:adecf6d02b1beab8d7c04bc36f22bb0e4c65a35eb0b4750b91693631d4081c70 \ 578 | --hash=sha256:af56229ea6527a849ac9fb154a059d7e32e77a8cba27e3e62a1e38d8808cb1a5 \ 579 | --hash=sha256:b324fa769577fc2c8f5efcd429cef5acbc17d63fe15ed16d6dcbac2c5eb00849 \ 580 | --hash=sha256:b5a07c4f29bf7cb0164664ef87e4aa25435dcc1f818d29842118b0ac1eb8e2b5 \ 581 | --hash=sha256:bad172aba822444b32eae54c2d5ab18cd7dee9814fd5c7ed026603b8cae2d05f \ 582 | --hash=sha256:bdca18b94c404af6ae5533cd1bc310c4931f7ac97c148bbfd2cd4bdd62b96253 \ 583 | --hash=sha256:be24a5867b8e3b9dd5c241de359a9a5217698ff616ac2daa47713ba2ebe30ad1 \ 584 | --hash=sha256:be86a26415a8b6af02cd8d782e3a9ae3872140a057f1cadf0133de685185c02b \ 585 | --hash=sha256:c66b7ff2527e18554030319b1376d81560ca0742c6e0b17ff1ee96624a5f1afd \ 586 | --hash=sha256:c8398a1b1951aaa330269c35335ae69744be166e67e0ebd9869bdc09426f3871 \ 587 | --hash=sha256:cad9545f5801a125f162d09ec9b724b7ad9b6440151b89645241d0120e119dcc \ 588 | --hash=sha256:cb6d161ae94fb35bb518b74bb06b7293299c15ba3bc099dccd6a5b7ae589aee3 \ 589 | --hash=sha256:d40682ac60b2a613d36d8d3a0cd14fbdf8e7e0618fbb40aa9fa7b796c9081584 \ 590 | --hash=sha256:d6128d431b8dfa888bf51c22a04d48bcb3d64431caf02b3cb943269f17fd2994 \ 591 | --hash=sha256:dbc466744a2db4b7ca05589f21ae1a35066afada2f803f92369f5877c100ef62 \ 592 | --hash=sha256:ddbef8b53cd16467fdbfa92a712eae46dd066aa19780681a2ce266e88fbc7165 \ 593 | --hash=sha256:e21cc00e4debe8f54c3ed7b9fcca540f46eee12762a9fa56feb8512fd9057161 \ 594 | --hash=sha256:eb52e826d16c09ef87132c6e360e1879c984f19a4f62d8a935345deac43f3c12 \ 595 | --hash=sha256:f0d9e7ba6a815a12c8575ba7887da4b72483e4cfc57179af10c9b937f3f9308f \ 596 | --hash=sha256:f1e931d9a92f628858a50f5bdffdfcf839aebe388b82f9d2ccd5d22a38a789dc \ 597 | --hash=sha256:f45808eda8b1d71308c5416ef3abe958f033fdbb356984fabbfc7887bed76b3f \ 598 | --hash=sha256:f6d39e42a0aa888122d1beb8ec0d4ddfb6c6b45aecb5ba4013c27e2f28657765 \ 599 | --hash=sha256:fc34fdd458ff77a2a00e3c86f899911f6f269d393ca5675842a6e92eea565bae 600 | requests==2.31.0 ; python_version >= "3.10" and python_version < "4.0" \ 601 | --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ 602 | --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 603 | rich==12.6.0 ; python_version >= "3.10" and python_version < "4.0" \ 604 | --hash=sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e \ 605 | --hash=sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0 606 | setuptools==68.0.0 ; python_version >= "3.10" and python_version < "4.0" \ 607 | --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \ 608 | --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235 609 | six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" \ 610 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ 611 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 612 | slugify==0.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 613 | --hash=sha256:c5703cc11c1a6947536f3ce8bb306766b8bb5a84a53717f5a703ce0f18235e4c 614 | snowballstemmer==2.2.0 ; python_version >= "3.10" and python_version < "4.0" \ 615 | --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ 616 | --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a 617 | sphinx==5.3.0 ; python_version >= "3.10" and python_version < "4.0" \ 618 | --hash=sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d \ 619 | --hash=sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5 620 | sphinxcontrib-applehelp==1.0.4 ; python_version >= "3.10" and python_version < "4.0" \ 621 | --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \ 622 | --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e 623 | sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.10" and python_version < "4.0" \ 624 | --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ 625 | --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 626 | sphinxcontrib-htmlhelp==2.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 627 | --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \ 628 | --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903 629 | sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 630 | --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ 631 | --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 632 | sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.10" and python_version < "4.0" \ 633 | --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ 634 | --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 635 | sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.10" and python_version < "4.0" \ 636 | --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ 637 | --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 638 | stack-data==0.6.2 ; python_version >= "3.10" and python_version < "4.0" \ 639 | --hash=sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815 \ 640 | --hash=sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8 641 | tomli==2.0.1 ; python_version >= "3.10" and python_version < "4.0" \ 642 | --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ 643 | --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f 644 | tornado==6.3.2 ; python_version >= "3.10" and python_version < "4.0" \ 645 | --hash=sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4 \ 646 | --hash=sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf \ 647 | --hash=sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d \ 648 | --hash=sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba \ 649 | --hash=sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe \ 650 | --hash=sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411 \ 651 | --hash=sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2 \ 652 | --hash=sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0 \ 653 | --hash=sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c \ 654 | --hash=sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f \ 655 | --hash=sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829 656 | tox==4.6.4 ; python_version >= "3.10" and python_version < "4.0" \ 657 | --hash=sha256:1b8f8ae08d6a5475cad9d508236c51ea060620126fd7c3c513d0f5c7f29cc776 \ 658 | --hash=sha256:5e2ad8845764706170d3dcaac171704513cc8a725655219acb62fe4380bdadda 659 | traitlets==5.9.0 ; python_version >= "3.10" and python_version < "4.0" \ 660 | --hash=sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8 \ 661 | --hash=sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9 662 | typeguard==3.0.2 ; python_version >= "3.10" and python_version < "4" \ 663 | --hash=sha256:bbe993854385284ab42fd5bd3bee6f6556577ce8b50696d6cb956d704f286c8e \ 664 | --hash=sha256:fee5297fdb28f8e9efcb8142b5ee219e02375509cd77ea9d270b5af826358d5a 665 | types-chevron==0.14.2.5 ; python_version >= "3.10" and python_version < "4.0" \ 666 | --hash=sha256:0f9b49b48aead9fd16f7065c9f78d4a1120b3449d0fd88049fc268b929ec501d \ 667 | --hash=sha256:dce11cfdb36bc565dfa9836252373ba32f37437cdc40703177169e875a000d1e 668 | typing-extensions==4.7.1 ; python_version >= "3.10" and python_version < "4.0" \ 669 | --hash=sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36 \ 670 | --hash=sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2 671 | urllib3==2.0.4 ; python_version >= "3.10" and python_version < "4.0" \ 672 | --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \ 673 | --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4 674 | virtualenv==20.24.1 ; python_version >= "3.10" and python_version < "4.0" \ 675 | --hash=sha256:01aacf8decd346cf9a865ae85c0cdc7f64c8caa07ff0d8b1dfc1733d10677442 \ 676 | --hash=sha256:2ef6a237c31629da6442b0bcaa3999748108c7166318d1f55cc9f8d7294e97bd 677 | wcwidth==0.2.6 ; python_version >= "3.10" and python_version < "4.0" \ 678 | --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \ 679 | --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0 680 | --------------------------------------------------------------------------------