🍭 Your own syntax you've always been dreaming of. 🍭
Sugaru is a lightweight completely customizable plugin system, that gives you an opportunity to do things like:
- Writing files followed your own syntax.
- Translating such files to ones with any other syntax.
- Any custom user-defined replacements (e.g. templates' replacements with environment variables).
- Converting files from one format to another (yaml --> json, toml --> ini, etc).
- Any other fascinating features you can imagine not listed above.
Python 3.7+
$ pip3 install sugaru
- Quickstart
- How it actually works
- Preparations under the hood
- How objects are loaded
- Examples
- Dependencies
- Changelog
It's better to see something once than to read documentation a thousand times. Let's follow this principle.
You are a kind of DevOps engineer. You write CI files every day. That's why you've learnt by heart some CI stages: literally, line-by-line.
One day you've fed up with copy-pasting/writing complete stages into the new project. And you have decided to reduce time and effort to write the same stage the hundredth time.
You took a close look to your stage once again:
stages:
- tests
pytest:
stage: "tests"
image: "python:3.12.0"
before_script:
- poetry install
script:
- poetry run pytest
And came up with idea to simply remove it. Indeed, the presence of some python-tests
stage can mean the same stage's code every time. Why not just generate such a stage then? You ended up with the syntax like:
stages: ""
python-tests: "python:3.12.0"
That's it! Looks good, doesn't it?
To translate python-tests
stage to tests
one let's write a couple of plugins that will do the job.
The first one will generate stages
section:
from typing import Any, Dict, List
def generate_stages(section_name: str, section: Any, sections: Dict[str, Any]) -> List[str]:
if section_name != "stages":
return section
known_stages: Dict[str, str] = {"python-tests": "tests"}
try:
return [known_stages[stage] for stage in sections if stage != "stages"]
except KeyError as unknown_stage:
raise ValueError(f"Unknown stage: {unknown_stage}") from None
The second plugin will generate python-tests
section:
def generate_tests(section_name: str, section: Any) -> Dict[str, Any]:
if section_name != "python-tests":
return section
py_image: str = section
return {
"stage": "tests",
"image": py_image,
"before_script": ["poetry install"],
"script": ["poetry run pytest"],
}
Let's put our plugins into the file called python_tests.py
. And put our awesome yml syntax into the file called .my-gitlab-ci.yml
.
To make it work simply type:
$ python3 -m sugaru .my-gitlab-ci.yml --plugin python_tests
That's it. You will see the correct .gitlab-ci.yml
syntax output on your screen.
There are some classes under the hood that work as a pipeline:
There is a file path as an entry point parameter (e.g. path to `.my-gitlab-ci.yml`). Then the output of every component is the input of the next component.- File Loader
- Output: file content as any JSON type
- Section Encoder
- Output: sections, i.e. mapping section-name: section-content.
- Plugin Executor
- Output: sections after every plugin execution
- Section Decoder
- Output: file content as any JSON type
- File Writer
- Output: the file with the content (including stdout)
There is also an interesting component called Object Loader. We'll discuss it later.
There is the default implementation for every component listed above.
Instead of using default components, you can define your own ones.
If you do so, such components are loaded by Object Loader component.
And what about the Object Loader component itself?
Actually, you can even implement a custom Object Loader component. Such the custom Object Loader will be loaded by default Object Loader first and then will replace the default one.
All custom defined objects -- including user plugins -- are loaded by interfaces.
To implement your own component you have to implement the interface dedicated to it.
For example, to implement custom Section Encoder you have to implement the following interface:
class SectionEncoder(Protocol):
def __call__(self, content: JSON) -> Dict[SecName, Section]:
...
The only exception is user Plugin. A plugin interface is defined as
class Plugin(Protocol):
def __call__(
self,
*,
section: Section,
section_name: str,
sections: Mapping[SecName, Section],
) -> Section:
...
To implement a plugin you should define any combination of interfaces' parameters. For example the following implementation also fits:
def empty_section(section_name: str) -> Any:
print(section_name)
return {}
The last thing you should known about interface's implementation is that type hints are not validated by sugaru (yet). It's up to you to use type hints for your own purposes.
Take a closer look to interfaces.py
file for more interfaces' detail.
You will find more examples with detail explanations inside examples folder.
Sugaru has some default dependencies, that are:
- typer. Typer makes sugaru more convenient to use.
- pyyaml. Sugaru manages to deal with yaml files by default.
You can also install loguru for more beautiful logs.
You can see the release history here: https://github.com/Mityuha/sugaru/releases/
Sugaru is MIT licensed code.