boilergen 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. boilergen-1.2.2/.github/workflows/publish.yml +28 -0
  2. boilergen-1.2.2/.gitignore +2 -0
  3. boilergen-1.2.2/.idea/.gitignore +3 -0
  4. boilergen-1.2.2/.idea/BoilerGen.iml +13 -0
  5. boilergen-1.2.2/.idea/inspectionProfiles/Project_Default.xml +59 -0
  6. boilergen-1.2.2/.idea/inspectionProfiles/profiles_settings.xml +6 -0
  7. boilergen-1.2.2/.idea/misc.xml +14 -0
  8. boilergen-1.2.2/.idea/modules.xml +8 -0
  9. boilergen-1.2.2/.idea/vcs.xml +7 -0
  10. boilergen-1.2.2/PKG-INFO +6 -0
  11. boilergen-1.2.2/README.md +187 -0
  12. boilergen-1.2.2/boilergen/__init__.py +0 -0
  13. boilergen-1.2.2/boilergen/__main__.py +4 -0
  14. boilergen-1.2.2/boilergen/builder/__init__.py +0 -0
  15. boilergen-1.2.2/boilergen/builder/generation_logic.py +32 -0
  16. boilergen-1.2.2/boilergen/builder/hooks.py +21 -0
  17. boilergen-1.2.2/boilergen/builder/output_selection.py +60 -0
  18. boilergen-1.2.2/boilergen/builder/parser/__init__.py +0 -0
  19. boilergen-1.2.2/boilergen/builder/parser/configs.py +161 -0
  20. boilergen-1.2.2/boilergen/builder/parser/injections.py +250 -0
  21. boilergen-1.2.2/boilergen/builder/parser/tags.py +54 -0
  22. boilergen-1.2.2/boilergen/builder/project_setup.py +251 -0
  23. boilergen-1.2.2/boilergen/cli/__init__.py +7 -0
  24. boilergen-1.2.2/boilergen/cli/commands.py +225 -0
  25. boilergen-1.2.2/boilergen/cli/run_config.py +10 -0
  26. boilergen-1.2.2/boilergen/core/__init__.py +0 -0
  27. boilergen-1.2.2/boilergen/core/display.py +273 -0
  28. boilergen-1.2.2/boilergen/core/navigator.py +333 -0
  29. boilergen-1.2.2/boilergen/core/template.py +42 -0
  30. boilergen-1.2.2/boilergen/core/template_finder.py +111 -0
  31. boilergen-1.2.2/boilergen/templates/gh-Workflows/Realease Version to Github/template/.github/workflows/flutter-release.yml +157 -0
  32. boilergen-1.2.2/boilergen/templates/gh-Workflows/Realease Version to Github/template.yaml +3 -0
  33. boilergen-1.2.2/boilergen.config +5 -0
  34. boilergen-1.2.2/pyproject.toml +13 -0
  35. boilergen-1.2.2/requirements.txt +9 -0
@@ -0,0 +1,28 @@
1
+ name: Upload Python Package to PyPI when a Release is Created
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ jobs:
8
+ pypi-publish:
9
+ name: Publish release to PyPI
10
+ runs-on: ubuntu-latest
11
+ environment:
12
+ name: pypi
13
+ url: https://pypi.org/p/boilergen
14
+ permissions:
15
+ id-token: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Set up Python
19
+ uses: actions/setup-python@v4
20
+ with:
21
+ python-version: "3.x"
22
+ - name: Install build backend
23
+ run: pip install build
24
+
25
+ - name: Build package
26
+ run: python -m build
27
+ - name: Publish package distributions to PyPI
28
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,2 @@
1
+ /boilergen.egg-info/
2
+ /.venv/
@@ -0,0 +1,3 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="PYTHON_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <excludeFolder url="file://$MODULE_DIR$/.venv" />
6
+ </content>
7
+ <orderEntry type="inheritedJdk" />
8
+ <orderEntry type="sourceFolder" forTests="false" />
9
+ </component>
10
+ <component name="PackageRequirementsSettings">
11
+ <option name="versionSpecifier" value="Strong equality (==x.y.z)" />
12
+ </component>
13
+ </module>
@@ -0,0 +1,59 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <profile version="1.0">
3
+ <option name="myName" value="Project Default" />
4
+ <inspection_tool class="PyAsyncCallInspection" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
5
+ <inspection_tool class="PyClassicStyleClassInspection" enabled="true" level="WARNING" enabled_by_default="true" />
6
+ <inspection_tool class="PyCompatibilityInspection" enabled="true" level="WARNING" enabled_by_default="true">
7
+ <option name="ourVersions">
8
+ <value>
9
+ <list size="1">
10
+ <item index="0" class="java.lang.String" itemvalue="3.13" />
11
+ </list>
12
+ </value>
13
+ </option>
14
+ </inspection_tool>
15
+ <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
16
+ <option name="ignoredPackages">
17
+ <value>
18
+ <list size="21">
19
+ <item index="0" class="java.lang.String" itemvalue="craiyon.py" />
20
+ <item index="1" class="java.lang.String" itemvalue="nextcord" />
21
+ <item index="2" class="java.lang.String" itemvalue="revChatGPT" />
22
+ <item index="3" class="java.lang.String" itemvalue="requests" />
23
+ <item index="4" class="java.lang.String" itemvalue="yfinance" />
24
+ <item index="5" class="java.lang.String" itemvalue="duolingo" />
25
+ <item index="6" class="java.lang.String" itemvalue="aiohttp" />
26
+ <item index="7" class="java.lang.String" itemvalue="pyenchant" />
27
+ <item index="8" class="java.lang.String" itemvalue="httpx" />
28
+ <item index="9" class="java.lang.String" itemvalue="googletrans" />
29
+ <item index="10" class="java.lang.String" itemvalue="google_trans_new" />
30
+ <item index="11" class="java.lang.String" itemvalue="openai" />
31
+ <item index="12" class="java.lang.String" itemvalue="fortnite-api" />
32
+ <item index="13" class="java.lang.String" itemvalue="geopy" />
33
+ <item index="14" class="java.lang.String" itemvalue="typing_extensions" />
34
+ <item index="15" class="java.lang.String" itemvalue="numpy" />
35
+ <item index="16" class="java.lang.String" itemvalue="mysql-connector-python" />
36
+ <item index="17" class="java.lang.String" itemvalue="pandas" />
37
+ <item index="18" class="java.lang.String" itemvalue="cryptography" />
38
+ <item index="19" class="java.lang.String" itemvalue="bcrypt" />
39
+ <item index="20" class="java.lang.String" itemvalue="matplotlib" />
40
+ </list>
41
+ </value>
42
+ </option>
43
+ </inspection_tool>
44
+ <inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
45
+ <option name="ignoredErrors">
46
+ <list>
47
+ <option value="E501" />
48
+ </list>
49
+ </option>
50
+ </inspection_tool>
51
+ <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
52
+ <option name="ignoredErrors">
53
+ <list>
54
+ <option value="N806" />
55
+ </list>
56
+ </option>
57
+ </inspection_tool>
58
+ </profile>
59
+ </component>
@@ -0,0 +1,6 @@
1
+ <component name="InspectionProjectProfileManager">
2
+ <settings>
3
+ <option name="USE_PROJECT_PROFILE" value="false" />
4
+ <version value="1.0" />
5
+ </settings>
6
+ </component>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="Black">
4
+ <option name="sdkName" value="Python 3.12 (BoilerGen)" />
5
+ </component>
6
+ <component name="DiscordProjectSettings">
7
+ <option name="show" value="ASK" />
8
+ <option name="description" value="" />
9
+ </component>
10
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (BoilerGen)" project-jdk-type="Python SDK" />
11
+ <component name="PyCharmProfessionalAdvertiser">
12
+ <option name="shown" value="true" />
13
+ </component>
14
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/BoilerGen.iml" filepath="$PROJECT_DIR$/.idea/BoilerGen.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ <mapping directory="$PROJECT_DIR$/output" vcs="Git" />
6
+ </component>
7
+ </project>
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: boilergen
3
+ Version: 1.2.2
4
+ Summary: CLI tool for creating template based boilerplate projects
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: typer[all]
@@ -0,0 +1,187 @@
1
+ # BoilerGen
2
+
3
+ BoilerGen creates files from reusable templates and injects necessary code (e.g. imports, registrations) directly into your project — without breaking existing code.
4
+
5
+ No more manual integration steps. No more forgotten imports. Just working boilerplate.
6
+
7
+ ## Setup
8
+
9
+ 1. [Install Python (3.11+) and pip](https://realpython.com/installing-python/)
10
+ 2. Clone this repository using `git clone https://github.com/HumanBot000/BoilerGen.git`
11
+ 3. Open your preferred command line and `cd` into the project directory.
12
+ 4. Run `pip install -r requirements.txt`
13
+ 5. Run `pip install -e .`
14
+ 6. [Set up your first templates](https://github.com/HumanBot000/BoilerGen?tab=readme-ov-file#templates)
15
+ 7. Run `boilergen create` and follow the instructions.
16
+ → All available commands can be accessed by `boilergen --help`.
17
+
18
+
19
+ ## Templates
20
+
21
+ Templates are pre-defined code snippets that can be reused across multiple projects with the same tech stack.
22
+ If you already have a boilerplate repository, you may need to edit some snippets to follow [BoilerGen's tagging rules](https://github.com/HumanBot000/BoilerGen?tab=readme-ov-file#tagging).
23
+
24
+ Templates are configured in the `boilergen/templates` directory and can be grouped into multiple subgroups [(see examples)](https://github.com/HumanBot000/BoilerGen/tree/master/boilergen/templates).
25
+
26
+ ## Template.yaml
27
+ Each template needs a `template.yaml` file for its Template Definition.
28
+ We highly encourage you to take a look at the [(Example Template repository)](https://github.com/HumanBot000/boilergen-templates).
29
+ Otherwise, here is a quick breakdown:
30
+ ```yaml
31
+ id: flask
32
+ label: "Flask Base App"
33
+ requires: [example]
34
+ config:
35
+ debug: True
36
+ port: 5000
37
+ ```
38
+ ### Fields
39
+ | id | The technical identifier for this template (Must be unique across all Templates) |
40
+ |---------- |-------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | label | The human-readable name of this Template (This will be shown in the Template browser) |
42
+ | requires | List of templates this template relies on (dependence management). This will be needed for injections. Use the `id` field of the template. |
43
+ | config | A Map of default values for [boilergen configurations](https://github.com/HumanBot000/BoilerGen?tab=readme-ov-file#configurations) |
44
+ ## Tagging
45
+
46
+ Often, multiple code snippets depend on each other and can't simply be copy-pasted and expected to work (e.g., special API routes need to be registered in the main API definition before startup). To simplify this process, BoilerGen uses a tagging system to automatically adjust your code.
47
+
48
+ ```python
49
+ # <<boilergen:imports
50
+ from flask import Flask
51
+ # boilergen:imports>>
52
+ ```
53
+
54
+ > Depending on your language of choice, you may need to edit the [comment syntax](https://gist.github.com/dk949/88b2652284234f723decaeb84db2576c). BoilerGen will comply with this, but the core syntax remains the same.
55
+
56
+ ### Tagging Syntax Explained
57
+
58
+ - `<<` indicates an opening tag.
59
+ - `>>` indicates a closing tag.
60
+ - The comment contains the keyword `boilergen`, identifying it as a special tag.
61
+ - After `boilergen`, a colon `:` and a unique identifier (e.g., `imports`) must follow.
62
+ - Everything between the tag lines is the tag's content and will be used for code injection.
63
+ > ⚠️ Tag opening and closing definitions **may not happen inline**. They need their own line with with no additional syntax.
64
+ >
65
+ > ⚠️ You **must not** use this exact syntax (`<<boilergen:...>>`) in any context not intended for BoilerGen. Doing so will corrupt your template.
66
+ >
67
+ > ⚠️ Identifiers must be unique **within** a template. We strongly recommend aslo keeping them unique **across** all templates to avoid confusion.
68
+
69
+ Example of unique identifiers:
70
+ ```text
71
+ boilergen:main-template_imports
72
+ boilergen:main-template_routes
73
+ ```
74
+ ---
75
+
76
+ ## Configurations
77
+
78
+ To simplify simple variations between projects (e.g., changing the app name or enabling debug mode), templates support configurable variables. These can be set in a `template.yaml` file or supplied interactively during `boilergen create`.
79
+
80
+ ```python
81
+ debug = bool("boilergen:config | debug | True")
82
+ ```
83
+
84
+ ### Configuration Syntax Explained
85
+
86
+ - Follows the same general structure as tagging.
87
+ - Does **not** require a unique identifier after the colon.
88
+ - The format is:
89
+ `boilergen:config | config_name | default_value`
90
+ - The `default_value` is optional, but must be provided at some point.
91
+
92
+ Example:
93
+ ```python
94
+ app.run(
95
+ host='boilergen:config | IP | "0.0.0.0"',
96
+ port="boilergen:config | port",
97
+ debug=debug
98
+ )
99
+ ```
100
+
101
+ > In this example:
102
+ > - `host` will be parsed as a `str ("0.0.0.0")` .
103
+ > - `debug` is already parsed using `bool(...)` above
104
+
105
+ We **strongly recommend** not placing configuration tags inside **inline comments**, as this may break the syntax highlighting and parsing in your language-specific editor or runtime. BoilerGen tries to **verify data types**, but we **strongly recommend** accepting them as **Strings** and parsing them individually, depending on your language,
106
+
107
+ ---
108
+
109
+ ### Configuration Precedence
110
+
111
+ The order of precedence for resolving configuration values is:
112
+
113
+ ```mermaid
114
+ stateDiagram-v2
115
+ step1: CLI input during project creation
116
+ step2: Value from template.yaml
117
+ step3: Default value in template
118
+ step1 --> step2
119
+ step2 --> step3
120
+ ```
121
+ ## Injections
122
+ Injections are a way to specify insertion/editing operations to files of foreign Templates.
123
+
124
+ ### Defining Injections
125
+ Injection definitions are located inside a special `injections;` folder at the parent level.
126
+ ```
127
+ boilergen/
128
+ ├── templates/
129
+ │ ├── base-template/
130
+ │ │ ├── template.yaml
131
+ │ │ └── template/
132
+ │ │ └── api/
133
+ │ │ └── test-file.txt
134
+ │ └── test-template/
135
+ │ ├── injections/
136
+ │ │ ├── data-file1.txt
137
+ │ │ ├── data-file2.txt
138
+ │ │ └── injections.yaml
139
+ │ ├── template/
140
+ │ └── template.yaml
141
+ ```
142
+
143
+ Generally you define injections in the extending template, not the base template.
144
+
145
+ #### injections.yaml
146
+ This File lays out a structure on how the injection behaves.
147
+ ```yaml
148
+ injections:
149
+ - target: base
150
+ at:
151
+ file: api/test-file.txt
152
+ tag: start
153
+ method:
154
+ insert:
155
+ - bottom
156
+ from: data-file1.txt
157
+
158
+ - target: base
159
+ at:
160
+ file: api/test-file.txt
161
+ tag: main
162
+ method:
163
+ replace:
164
+ from: data-file2.txt
165
+ ```
166
+ This setup takes the whole content of `data-file1.txt` and inserts right before the closing definition of the "start" tag at `api/test-file.txt` inside the base template. It does the same for data-file2.txt but replaces the whole "main" section of the file.
167
+
168
+ ##### Fields
169
+ | name | description | note | possible values |
170
+ |----------- |-------------------------------------------------------------------------------------- |---------------------------------------------------------------------------- |--------------------------- |
171
+ | at | The relative path to the file to inject into | This must be defined inside the `requires` part of the template.yaml file. | |
172
+ | tag | The identifier of the tag to inject into | Can't be used alongside `line` in the same injection. | |
173
+ | line | A single integer describing the line the injection affects | Can't be used alongside `tag` in the same injection. | |
174
+ | method | what to do with the current tag content | | insert, replace |
175
+ | -> insert | Where to insert the new content | Can only be used when method is `insert` | above, below, top, bottom |
176
+ | from | The relative path to the file inside the injecting template to pull the content from | | |
177
+
178
+ ## Hooks
179
+ Hooks are a way to specify operations to be executed on specific times during the project generation process.
180
+
181
+ ### Defining Hooks
182
+ Hooks are configured in the `boilergen/hooks` directory. The file name specifies the event the hook is triggered on.
183
+ Currently supported events are:
184
+ - `pre-generation.txt`
185
+ - `post-generation.txt`
186
+
187
+ Inside those files, you can define shell commands to be executed. Each command is separated by a newline. The execution order is top to bottom.
File without changes
@@ -0,0 +1,4 @@
1
+ from boilergen.cli.commands import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
File without changes
@@ -0,0 +1,32 @@
1
+ import boilergen.cli.run_config
2
+ from boilergen.builder.parser.tags import TemplateFile
3
+
4
+
5
+ def generate_file_content_data(file: TemplateFile, run_config: boilergen.cli.run_config.RunConfig):
6
+ text = file.content
7
+ # Configs
8
+ for config in sorted(file.configs, key=lambda c: c.replacement_start, reverse=True):
9
+ start = config.replacement_start
10
+ end = config.replacement_end
11
+ if start > 0 and end < len(text):
12
+ if not run_config.disable_quote_parsing_for_configs:
13
+ if text[start - 1] in ['"', "'"] and text[end] in ['"', "'"]:
14
+ start -= 1
15
+ end += 1
16
+ text = text[:start] + config.insertion_value + text[end:]
17
+
18
+ lines = text.splitlines()
19
+ # Tag removal
20
+ for index,tag in enumerate(sorted(file.tags, key=lambda t: t.line_start, reverse=True)):
21
+ lines[tag.line_start - 1] = ""
22
+ lines[tag.line_end - 1] = ""
23
+ """del lines[tag.line_start]
24
+ del lines[tag.line_end-1]
25
+ for other_tag in sorted(file.tags, key=lambda t: t.line_start, reverse=True)[index:]:
26
+ other_tag.line_start -= 2
27
+ other_tag.line_end -= 2
28
+ """
29
+ text = "\n".join(lines)
30
+ file.content = text
31
+
32
+
@@ -0,0 +1,21 @@
1
+ import os
2
+
3
+
4
+ def process_post_generation_hook(output_path: str, hook_position: str):
5
+ hook_position = os.path.join(hook_position, "hooks")
6
+ os.chdir(output_path)
7
+ if not os.path.exists(os.path.join(hook_position, "post-generation.txt")):
8
+ return
9
+ with open(os.path.join(hook_position, "post-generation.txt"), "r") as f:
10
+ for line in f.readlines():
11
+ os.system(line)
12
+
13
+
14
+ def process_pre_generation_hook(output_path: str, hook_position: str):
15
+ hook_position = os.path.join(hook_position, "hooks")
16
+ os.chdir(output_path)
17
+ if not os.path.exists(os.path.join(hook_position, "pre-generation.txt")):
18
+ return
19
+ with open(os.path.join(hook_position, "pre-generation.txt"), "r") as f:
20
+ for line in f.readlines():
21
+ os.system(line)
@@ -0,0 +1,60 @@
1
+ import os
2
+ import shutil
3
+ import stat
4
+ from typing import List
5
+
6
+ import questionary
7
+ import typer
8
+
9
+ import boilergen.core.template
10
+ from boilergen.builder.hooks import process_post_generation_hook, process_pre_generation_hook
11
+ from boilergen.builder.project_setup import create_project
12
+ import boilergen.core.display
13
+
14
+ def force_remove_readonly(func, path, _):
15
+ os.chmod(path, stat.S_IWRITE)
16
+ func(path)
17
+
18
+
19
+ def clear_cloned_repo(template_dir, minimal_ui, console):
20
+ if template_dir.endswith(f"cloned_templates"):
21
+ if minimal_ui:
22
+ print("Removing remote templates...")
23
+ else:
24
+ console.print("[red]Removing remote templates...[/red]")
25
+ shutil.rmtree(template_dir, onerror=force_remove_readonly)
26
+
27
+
28
+ def ask_for_output_location(selected_templates: List[boilergen.core.template.Template], run_config, template_dir):
29
+ output_selection = questionary.prompt(
30
+ [
31
+ {
32
+ "type": "input",
33
+ "name": "output",
34
+ "message": "Where do you want to generate the output?",
35
+ "default": os.path.join(os.getcwd(), "output"),
36
+ }
37
+ ]
38
+ )["output"]
39
+ if not os.path.exists(output_selection):
40
+ os.makedirs(output_selection, exist_ok=True)
41
+ else:
42
+ if run_config.clear_output:
43
+ if typer.confirm(
44
+ f"Output directory {output_selection} does already exist. Do you want to overwrite it? {typer.style("(This will delete existing data!)", fg=typer.colors.RED)}",
45
+ default=False
46
+ ):
47
+ try:
48
+ shutil.rmtree(output_selection) # recursively delete
49
+ os.makedirs(output_selection, exist_ok=True) # recreate clean output dir
50
+ except PermissionError:
51
+ raise PermissionError(
52
+ "Permission denied while trying to delete the output directory. Try running with admin privileges.")
53
+ else:
54
+ raise ValueError(
55
+ f"Output directory {output_selection} does already exist. Run with --clear-output to overwrite it.")
56
+ template_dir = os.sep.join(template_dir.split(os.sep)[:-1])
57
+ process_pre_generation_hook(output_selection,template_dir)
58
+ create_project(output_selection, selected_templates, run_config)
59
+ process_post_generation_hook(output_selection,template_dir)
60
+ clear_cloned_repo(template_dir, run_config.minimal_ui, boilergen.core.display.console)
File without changes
@@ -0,0 +1,161 @@
1
+ # todo The regex and quote detection was written entirely by AI, seems to work but unit tests are top priority
2
+ import re
3
+ from typing import Union
4
+
5
+
6
+ class NotDefinedType:
7
+ def __repr__(self):
8
+ return "NOT_DEFINED"
9
+
10
+ def __str__(self):
11
+ return "NOT_DEFINED"
12
+
13
+
14
+ NOT_DEFINED = NotDefinedType()
15
+
16
+ ValueType = Union[str, bool, None, NotDefinedType]
17
+
18
+
19
+ class ValueConfig:
20
+ def __init__(self, identifier: str, replacement_start: int, replacement_end: int,
21
+ in_template_value: ValueType, yaml_value: ValueType, cli_value: ValueType):
22
+ self.identifier = identifier
23
+ self.replacement_start = replacement_start
24
+ self.replacement_end = replacement_end
25
+ self.in_template_value = in_template_value
26
+ self.yaml_value = yaml_value
27
+ self.cli_value = cli_value
28
+
29
+ @property
30
+ def insertion_value(self):
31
+ value_order = [self.cli_value, self.yaml_value, self.in_template_value]
32
+ for value in value_order:
33
+ if value is not NOT_DEFINED:
34
+ return value
35
+ return NOT_DEFINED
36
+
37
+ def __repr__(self):
38
+ return self.__str__()
39
+
40
+ def __str__(self):
41
+ return f"ValueConfig(identifier={self.identifier}, in_template_value={self.in_template_value}, yaml_value={self.yaml_value}, cli_value={self.cli_value})"
42
+
43
+
44
+ def extract_configs(file_content: str):
45
+ full_pattern = re.compile(
46
+ r'boilergen:config\s*\|\s*'
47
+ r'([^\s|]+)' # identifier
48
+ r'(?:\s*\|\s*'
49
+ r'([^"\']*(?:"[^"]*"|\'[^\']*\'[^"\']*)*)' # optional value (may contain quoted parts)
50
+ r')?'
51
+ )
52
+
53
+ offset = 0
54
+ configs = []
55
+
56
+ for line_number, line in enumerate(file_content.splitlines(keepends=True), start=1):
57
+ in_quotes = []
58
+ i = 0
59
+ while i < len(line):
60
+ if line[i] in ('"', "'"):
61
+ quote_char = line[i]
62
+ start_quote = i
63
+ i += 1
64
+ while i < len(line) and line[i] != quote_char:
65
+ if line[i] == '\\':
66
+ i += 2
67
+ else:
68
+ i += 1
69
+ if i < len(line):
70
+ in_quotes.append((start_quote, i, quote_char, line[start_quote:i + 1]))
71
+ i += 1
72
+ else:
73
+ i += 1
74
+
75
+ # 1. Matches **within quotes** (current logic)
76
+ for start_quote, end_quote, quote_char, quoted_content in in_quotes:
77
+ inner_content = quoted_content[1:-1]
78
+ for m in full_pattern.finditer(inner_content):
79
+ match_start = offset + start_quote + 1 + m.start()
80
+ match_end = offset + start_quote + 1 + m.end()
81
+ identifier = m.group(1).strip()
82
+ raw_value = m.group(2).strip() if m.group(2) is not None else None
83
+ interpreted_value = interpret_value(raw_value, quote_char)
84
+
85
+ config = ValueConfig(
86
+ identifier=identifier,
87
+ replacement_start=match_start,
88
+ replacement_end=match_end,
89
+ in_template_value=interpreted_value,
90
+ yaml_value=NOT_DEFINED,
91
+ cli_value=NOT_DEFINED
92
+ )
93
+ configs.append(config)
94
+
95
+ # 2. Matches **outside of quotes**
96
+ quote_ranges = [(s, e) for s, e, *_ in in_quotes]
97
+
98
+ def is_outside_quotes(pos):
99
+ return not any(start <= pos < end + 1 for start, end in quote_ranges)
100
+
101
+ for m in full_pattern.finditer(line):
102
+ if is_outside_quotes(m.start()):
103
+ match_start = offset + m.start()
104
+ match_end = offset + m.end()
105
+ identifier = m.group(1).strip()
106
+ raw_value = m.group(2).strip() if m.group(2) is not None else None
107
+ interpreted_value = interpret_value(raw_value, None)
108
+
109
+ config = ValueConfig(
110
+ identifier=identifier,
111
+ replacement_start=match_start,
112
+ replacement_end=match_end,
113
+ in_template_value=interpreted_value,
114
+ yaml_value=NOT_DEFINED,
115
+ cli_value=NOT_DEFINED
116
+ )
117
+ configs.append(config)
118
+
119
+ offset += len(line)
120
+
121
+ return configs
122
+
123
+
124
+
125
+ def fetch_yaml_configs(configs: list[ValueConfig], yaml_data: dict):
126
+ for config in configs:
127
+ if isinstance(yaml_data, dict) and "config" in yaml_data:
128
+ if config.identifier in yaml_data["config"]:
129
+ config.yaml_value = yaml_data["config"][config.identifier]
130
+
131
+
132
+ def interpret_value(raw: Union[str, None, NotDefinedType], outer_quote: Union[str, None]) -> ValueType:
133
+ """
134
+ Interpret the raw value depending on the outer quote context.
135
+
136
+ - If raw is None or NOT_DEFINED, return NOT_DEFINED.
137
+ - Strip raw.
138
+ - If outer_quote is set:
139
+ - If raw starts and ends with matching quotes, preserve raw (including quotes).
140
+ - Else return raw stripped (no added quotes).
141
+ - If outer_quote is None:
142
+ - Return raw stripped (no quotes stripped or added).
143
+ """
144
+ if raw is None or raw is NOT_DEFINED:
145
+ return NOT_DEFINED
146
+
147
+ txt = raw.strip()
148
+ if not txt: # Empty string case
149
+ return txt
150
+ if outer_quote:
151
+ # We're inside outer quotes like "boilergen:config | debug | True"
152
+ if len(txt) >= 2 and txt[0] == txt[-1] and txt[0] in ('"', "'"):
153
+ # Value has explicit inner quotes: "boilergen:config | debug | 'True'" -> 'True'
154
+ # or 'boilergen:config | host | "0.0.0.0"' -> "0.0.0.0"
155
+ return txt
156
+ else:
157
+ # Value has no inner quotes: "boilergen:config | debug | True" -> True
158
+ return txt
159
+ else:
160
+ # No outer quote context: just return stripped value
161
+ return txt