proj-flow 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

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 (54) hide show
  1. proj_flow/__init__.py +1 -1
  2. proj_flow/__main__.py +1 -1
  3. proj_flow/api/__init__.py +11 -2
  4. proj_flow/api/arg.py +14 -6
  5. proj_flow/api/env.py +15 -35
  6. proj_flow/api/release.py +99 -0
  7. proj_flow/api/step.py +12 -2
  8. proj_flow/base/__init__.py +2 -2
  9. proj_flow/base/inspect.py +15 -44
  10. proj_flow/base/plugins.py +41 -2
  11. proj_flow/base/registry.py +105 -0
  12. proj_flow/cli/__init__.py +55 -0
  13. proj_flow/cli/argument.py +447 -0
  14. proj_flow/{flow/cli → cli}/finder.py +1 -1
  15. proj_flow/ext/__init__.py +6 -0
  16. proj_flow/ext/github/__init__.py +11 -0
  17. proj_flow/ext/github/cli.py +118 -0
  18. proj_flow/ext/github/hosting.py +19 -0
  19. proj_flow/ext/markdown_changelist.py +14 -0
  20. proj_flow/ext/python/__init__.py +10 -0
  21. proj_flow/ext/python/rtdocs.py +238 -0
  22. proj_flow/ext/python/steps.py +71 -0
  23. proj_flow/ext/python/version.py +98 -0
  24. proj_flow/ext/re_structured_changelist.py +14 -0
  25. proj_flow/flow/__init__.py +3 -3
  26. proj_flow/flow/configs.py +21 -5
  27. proj_flow/flow/dependency.py +8 -6
  28. proj_flow/flow/steps.py +6 -9
  29. proj_flow/log/__init__.py +10 -2
  30. proj_flow/log/commit.py +19 -4
  31. proj_flow/log/error.py +31 -0
  32. proj_flow/log/hosting/github.py +10 -6
  33. proj_flow/log/msg.py +23 -0
  34. proj_flow/log/release.py +112 -21
  35. proj_flow/log/rich_text/__init__.py +0 -12
  36. proj_flow/log/rich_text/api.py +10 -4
  37. proj_flow/minimal/__init__.py +11 -0
  38. proj_flow/{plugins/commands → minimal}/bootstrap.py +2 -2
  39. proj_flow/{plugins/commands → minimal}/list.py +12 -10
  40. proj_flow/{plugins/commands → minimal}/run.py +20 -11
  41. proj_flow/{plugins/commands → minimal}/system.py +2 -2
  42. proj_flow/plugins/__init__.py +1 -1
  43. proj_flow/template/layers/base/.flow/matrix.yml +1 -1
  44. proj_flow/template/layers/cmake/CMakeLists.txt.mustache +1 -1
  45. proj_flow/template/layers/github_actions/.github/workflows/build.yml +1 -1
  46. proj_flow/template/layers/github_social/.github/ISSUE_TEMPLATE/feature_request.md.mustache +1 -1
  47. {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/METADATA +6 -5
  48. {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/RECORD +51 -37
  49. proj_flow-0.9.0.dist-info/entry_points.txt +2 -0
  50. proj_flow/flow/cli/__init__.py +0 -66
  51. proj_flow/flow/cli/cmds.py +0 -385
  52. proj_flow-0.8.1.dist-info/entry_points.txt +0 -2
  53. {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/WHEEL +0 -0
  54. {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,447 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.cli.arguments** provides command-line builders and runners,
6
+ supporting the functions decorated with :func:`@arg.command()
7
+ <proj_flow.api.arg.command>`.
8
+ """
9
+
10
+ import argparse
11
+ import typing
12
+ from dataclasses import dataclass, field
13
+
14
+ from proj_flow import __version__
15
+ from proj_flow.api import arg, completers, env, step
16
+ from proj_flow.base import inspect as _inspect
17
+ from proj_flow.base import registry
18
+ from proj_flow.flow import configs
19
+
20
+
21
+ class Completer(typing.Protocol):
22
+ def __call__(self, **kwarg) -> typing.Any: ...
23
+
24
+
25
+ class Action(typing.Protocol):
26
+ completer: Completer
27
+
28
+
29
+ class Subparsers(typing.Protocol):
30
+ parent: "Parser"
31
+
32
+ def add_parser(*args, **kwargs) -> "Parser": ...
33
+
34
+
35
+ class Parser(argparse.ArgumentParser):
36
+ flow: env.FlowConfig
37
+ menu: typing.List["Command"]
38
+ shortcuts: typing.Dict[str, typing.List[str]]
39
+
40
+ def __init__(self, **kwargs):
41
+ super().__init__(**kwargs)
42
+
43
+ def add_subparsers(self, **kwargs) -> Subparsers:
44
+ return typing.cast(Subparsers, super().add_subparsers(**kwargs))
45
+
46
+ def find_and_run_command(self, args: argparse.Namespace):
47
+ commands = {entry.name for entry in self.menu}
48
+ aliases = self.flow.aliases
49
+
50
+ rt = env.Runtime(args, self.flow)
51
+
52
+ if rt.verbose:
53
+ verbose_info(self.menu)
54
+ step.verbose_info()
55
+ registry.verbose_info()
56
+
57
+ if args.command in commands:
58
+ command = _first(lambda command: command.name == args.command, self.menu)
59
+ if command:
60
+ return command.run(args, rt)
61
+ elif args.command in {alias.name for alias in aliases}:
62
+ command = _first(lambda command: command.name == "run", self.menu)
63
+ alias = _first(lambda alias: alias.name == args.command, aliases)
64
+ if command and alias:
65
+ args.cli_steps.append(",".join(alias.steps))
66
+ return command.run(args, rt)
67
+
68
+ lines = ["the command arguments are required; known commands:", ""]
69
+ for command in self.menu:
70
+ lines.append(f" - {command.name}: {command.doc}")
71
+ for alias in aliases:
72
+ lines.append(f" - {alias.name}: {alias.doc}")
73
+
74
+ self.error("\n".join(lines))
75
+
76
+
77
+ def build_argparser(flow_cfg: env.FlowConfig):
78
+ parser = Parser(
79
+ prog="proj-flow",
80
+ description="C++ project maintenance, automated",
81
+ add_help=False,
82
+ )
83
+ parser.add_argument(
84
+ "-h", "--help", action="help", help="Show this help message and exit"
85
+ )
86
+ parser.add_argument(
87
+ "-v",
88
+ "--version",
89
+ action="version",
90
+ default=argparse.SUPPRESS,
91
+ version=f"%(prog)s version {__version__}",
92
+ help="Show proj-flow's version and exit",
93
+ )
94
+ parser.add_argument(
95
+ "-C",
96
+ metavar="dir",
97
+ nargs="?",
98
+ help="Run as if proj-flow was started in <dir> instead of the current "
99
+ "working directory. This directory must exist.",
100
+ ).completer = completers.cd_completer # type: ignore
101
+
102
+ menu = _build_menu(arg.get_commands(), None)
103
+ _argparse_visit_all(parser, flow_cfg, menu.children)
104
+ return parser
105
+
106
+
107
+ def expand_shortcuts(parser: Parser, args: argparse.Namespace):
108
+ args_kwargs = dict(args._get_kwargs())
109
+ shortcuts: typing.Dict[str, typing.List[str]] = parser.shortcuts
110
+ for key in shortcuts:
111
+ try:
112
+ if not args_kwargs[key]:
113
+ continue
114
+ typing.cast(typing.List[str], args.configs).extend(shortcuts[key])
115
+ break
116
+ except KeyError:
117
+ continue
118
+
119
+
120
+ @dataclass
121
+ class AdditionalArgument:
122
+ name: str
123
+ ctor: callable # type: ignore
124
+
125
+ def create(self, rt: env.Runtime, args: argparse.Namespace, menu: "Command"):
126
+ if self.ctor == env.Runtime:
127
+ return rt
128
+ if self.ctor == Command:
129
+ return menu
130
+ return self.ctor(rt, args)
131
+
132
+
133
+ @dataclass
134
+ class AnnotatedArgument:
135
+ name: str
136
+ argument: arg.Argument
137
+
138
+ def argparse_visit(self, parser: argparse.ArgumentParser):
139
+ return self.argument.visit(parser, self.name)
140
+
141
+
142
+ AnyArgument = typing.Union[AdditionalArgument, AnnotatedArgument]
143
+
144
+
145
+ @dataclass
146
+ class Command:
147
+ name: str
148
+ doc: str
149
+ entry: callable # type: ignore
150
+ annotated: typing.List[AnnotatedArgument]
151
+ additional: typing.List[AdditionalArgument]
152
+ parent: typing.Optional["Command"]
153
+ children: typing.List["Command"] = field(default_factory=list)
154
+
155
+ def argparse_visit(
156
+ self,
157
+ subparsers: Subparsers,
158
+ alias: typing.Optional[str] = None,
159
+ doc: typing.Optional[str] = None,
160
+ level=0,
161
+ ):
162
+ if not doc:
163
+ doc = self.doc
164
+ if not alias:
165
+ alias = self.name
166
+
167
+ parser = subparsers.add_parser(
168
+ alias, help=doc.split("\n\n")[0], description=doc, add_help=False
169
+ )
170
+
171
+ parent = subparsers.parent
172
+ parser.flow = parent.flow
173
+ parser.shortcuts = parent.shortcuts
174
+
175
+ assert parent.flow is not None
176
+ assert parent.shortcuts is not None
177
+
178
+ _argparse_runtime_visit(parser)
179
+
180
+ has_config = False
181
+ for additional in self.additional:
182
+ if additional.ctor == configs.Configs:
183
+ has_config = True
184
+ break
185
+
186
+ if has_config:
187
+ _argparse_config_visit(parser)
188
+
189
+ for arg in self.annotated:
190
+ arg.argparse_visit(parser)
191
+
192
+ if len(self.children):
193
+ subparsers = parser.add_subparsers(
194
+ dest=f"command_{level}",
195
+ metavar="{command}",
196
+ help="Known command name, see below",
197
+ )
198
+ subparsers.parent = parser # type: ignore
199
+
200
+ for entry in self.children:
201
+ entry.argparse_visit(subparsers, level=level + 1)
202
+
203
+ def run(self, args: argparse.Namespace, rt: env.Runtime, level=0):
204
+ if level == 0 and rt.only_host:
205
+ rt.only_host = self.name == "run"
206
+
207
+ subcommand_name = None
208
+
209
+ if len(self.children):
210
+ subcommand_attribute = f"command_{level}"
211
+ if hasattr(args, subcommand_attribute):
212
+ subcommand_name = getattr(args, subcommand_attribute)
213
+
214
+ if subcommand_name is not None:
215
+ subcommand = _first(
216
+ lambda command: command.name == subcommand_name, self.children
217
+ )
218
+ if not subcommand:
219
+ rt.fatal(f"cannot find {subcommand_name}")
220
+
221
+ return subcommand.run(args, rt, level=level + 1)
222
+
223
+ kwargs = {}
224
+ for arg in self.annotated:
225
+ kwargs[arg.name] = getattr(args, arg.name, None)
226
+
227
+ for additional in self.additional:
228
+ arg = additional.create(rt, args, self)
229
+ kwargs[additional.name] = arg
230
+
231
+ result = self.entry(**kwargs)
232
+ return 0 if result is None else result
233
+
234
+
235
+ T = typing.TypeVar("T")
236
+
237
+
238
+ def _first(
239
+ fltr: typing.Callable[[T], bool], items: typing.Iterable[T]
240
+ ) -> typing.Optional[T]:
241
+ try:
242
+ return next(filter(fltr, items))
243
+ except StopIteration:
244
+ return None
245
+
246
+
247
+ def _argparse_visit_all(
248
+ parser: Parser, cfg: env.FlowConfig, menu: typing.List["Command"]
249
+ ):
250
+ shortcut_configs = _build_run_shortcuts(cfg)
251
+
252
+ parser.menu = menu
253
+ parser.flow = cfg
254
+ parser.shortcuts = shortcut_configs
255
+
256
+ subparsers = parser.add_subparsers(
257
+ dest="command", metavar="{command}", help="Known command name, see below"
258
+ )
259
+
260
+ subparsers.parent = parser # type: ignore
261
+
262
+ run: typing.Optional[Command] = None
263
+ for entry in menu:
264
+ entry.argparse_visit(subparsers)
265
+ if entry.name == "run":
266
+ run = entry
267
+
268
+ if run is not None and len(cfg.aliases) > 0:
269
+ commands = {entry.name for entry in menu}
270
+ cfg.aliases = [alias for alias in cfg.aliases if alias.name not in commands]
271
+ for alias in cfg.aliases:
272
+ run.argparse_visit(subparsers, alias=alias.name, doc=alias.doc)
273
+ else:
274
+ cfg.aliases = []
275
+
276
+ pass
277
+
278
+
279
+ def _build_menu(cmd: arg._Command, parent: typing.Optional[Command]):
280
+ name = cmd.name
281
+ doc = cmd.doc or ""
282
+ entry = cmd.entry or (lambda: 0)
283
+
284
+ args = _extract_args(entry)
285
+ additional = [entry for entry in args if isinstance(entry, AdditionalArgument)]
286
+ annotated = [entry for entry in args if isinstance(entry, AnnotatedArgument)]
287
+
288
+ current = Command(
289
+ name=name,
290
+ doc=doc,
291
+ entry=entry,
292
+ annotated=annotated,
293
+ additional=additional,
294
+ parent=parent,
295
+ )
296
+ for child in cmd.subs.values():
297
+ current.children.append(_build_menu(child, current))
298
+
299
+ return current
300
+
301
+
302
+ def _extract_arg(argument: _inspect.Argument):
303
+ for ctor in [configs.Configs, env.Runtime, Command]:
304
+ if argument.type is ctor:
305
+ return AdditionalArgument(argument.name, ctor)
306
+
307
+ metadata: typing.Optional[arg.Argument] = _first(
308
+ lambda meta: isinstance(meta, arg.Argument), argument.metadata
309
+ )
310
+
311
+ if metadata is None or argument.type is None:
312
+ return None
313
+
314
+ optional = metadata.opt
315
+ if optional is None:
316
+ NoneType = type(None)
317
+ is_union = typing.get_origin(argument.type) is typing.Union
318
+ optional = is_union and NoneType in typing.get_args(argument.type)
319
+ metadata.opt = optional
320
+
321
+ return AnnotatedArgument(argument.name, metadata)
322
+
323
+
324
+ def _extract_args(entry: callable): # type: ignore
325
+ args_with_possible_nones = map(_extract_arg, _inspect.signature(entry))
326
+ args = filter(lambda item: item is not None, args_with_possible_nones)
327
+ return typing.cast(typing.List[AnyArgument], list(args))
328
+
329
+
330
+ def _build_run_shortcuts(cfg):
331
+ shortcut_configs: typing.Dict[str, typing.List[str]] = {}
332
+ args: typing.List[typing.Tuple[str, typing.List[str], bool, bool]] = []
333
+
334
+ shortcuts = cfg.shortcuts
335
+ for shortcut_name in sorted(shortcuts.keys()):
336
+ has_os = False
337
+ has_compiler = False
338
+ shortcut = shortcuts[shortcut_name]
339
+ config: typing.List[str] = []
340
+ for key in sorted(shortcut.keys()):
341
+ has_os = has_os or key == "os"
342
+ has_compiler = has_compiler or key == "os"
343
+ value = shortcut[key]
344
+ if isinstance(value, list):
345
+ for v in value:
346
+ config.append(f"{key}={_shortcut_value(v)}")
347
+ else:
348
+ config.append(f"{key}={_shortcut_value(value)}")
349
+ if len(config) > 0:
350
+ args.append((shortcut_name, config, has_os, has_compiler))
351
+
352
+ if len(args):
353
+ os_prefix = f"os={env.platform}"
354
+ compiler_prefix = f"compiler={env.default_compiler()}"
355
+
356
+ for shortcut_name, config, has_os, has_compiler in args:
357
+ if not has_compiler:
358
+ config.insert(0, compiler_prefix)
359
+ if not has_os:
360
+ config.insert(0, os_prefix)
361
+ shortcut_configs[shortcut_name] = config
362
+
363
+ return shortcut_configs
364
+
365
+
366
+ def _shortcut_value(value) -> str:
367
+ if isinstance(value, bool):
368
+ return "ON" if value else "OFF"
369
+ return str(value)
370
+
371
+
372
+ def _argparse_runtime_visit(parser: Parser):
373
+ parser.add_argument(
374
+ "-h",
375
+ "--help",
376
+ action="help",
377
+ default=argparse.SUPPRESS,
378
+ help="Show this help message and exit",
379
+ )
380
+
381
+ parser.add_argument(
382
+ "--dry-run",
383
+ action="store_true",
384
+ required=False,
385
+ help="Print steps and commands, do nothing",
386
+ )
387
+
388
+ verbosity = parser.add_mutually_exclusive_group()
389
+ verbosity.add_argument(
390
+ "--silent",
391
+ action="store_true",
392
+ required=False,
393
+ help="Remove most of the output",
394
+ )
395
+ verbosity.add_argument(
396
+ "--verbose",
397
+ action="store_true",
398
+ required=False,
399
+ help="Add even more output",
400
+ )
401
+
402
+
403
+ def _argparse_config_visit(parser: Parser):
404
+ parser.add_argument(
405
+ "-D",
406
+ dest="configs",
407
+ metavar="key=value",
408
+ nargs="*",
409
+ action="store",
410
+ default=[],
411
+ help="Run only builds on matching configs. The key is one of "
412
+ 'the keys into "matrix" object in .flow/matrix.yml definition '
413
+ "and the value is one of the possible values for that key. In "
414
+ "case of boolean flags, such as sanitizer, the true value is "
415
+ 'one of "true", "on", "yes", "1" and "with-<key>", '
416
+ 'i.e. "with-sanitizer" for sanitizer.'
417
+ " "
418
+ "If given key is never used, all values from .flow/matrix.yaml "
419
+ "for that key are used. Otherwise, only values from command "
420
+ "line are used.",
421
+ ).completer = completers.matrix_completer # type: ignore
422
+
423
+ parser.add_argument(
424
+ "--official",
425
+ action="store_true",
426
+ required=False,
427
+ help="Cut matrix to release builds only",
428
+ )
429
+
430
+ if len(parser.shortcuts):
431
+ group = parser.add_mutually_exclusive_group()
432
+
433
+ for shortcut_name in sorted(parser.shortcuts.keys()):
434
+ config = parser.shortcuts[shortcut_name]
435
+ group.add_argument(
436
+ f"--{shortcut_name}",
437
+ required=False,
438
+ action="store_true",
439
+ help=f'Shortcut for "-D {" ".join(config)}"',
440
+ )
441
+
442
+ def verbose_info(commands: typing.List[Command], prefix = ""):
443
+ for command in commands:
444
+ cli = f"{prefix} {command.name}" if prefix else command.name
445
+ if command.entry is not None:
446
+ print(f"-- Command: adding `{cli}` from `{command.entry.__module__}.{command.entry.__name__}(...)`")
447
+ verbose_info(command.children, cli)
@@ -2,7 +2,7 @@
2
2
  # This code is licensed under MIT license (see LICENSE for details)
3
3
 
4
4
  """
5
- The **proj_flow.flow.cli.finder** extends the :py:class:`argcomplete.finders.CompletionFinder`
5
+ The **proj_flow.cli.finder** extends the :py:class:`argcomplete.finders.CompletionFinder`
6
6
  to be able to see proper :class:`api.env.FlowConfig`, when completed invocation
7
7
  contains ``-C`` argument.
8
8
  """
@@ -0,0 +1,6 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext** contains standard set of extensions.
6
+ """
@@ -0,0 +1,11 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.github** provides GitHub support through CLI and GitHub
6
+ hosting plugin.
7
+ """
8
+
9
+ from . import cli, hosting
10
+
11
+ __all__ = ["cli", "hosting"]
@@ -0,0 +1,118 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.github.cli** adds the ``github`` command, replacing the
6
+ old ``ci`` code. Additionally, it provides ``github matrix`` and ``github
7
+ release`` commands. It will soon also have ``github publish``, finishing the
8
+ job started in ``release``.
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import os
14
+ import sys
15
+ import typing
16
+
17
+ from proj_flow import log
18
+ from proj_flow.api import arg, env
19
+ from proj_flow.flow.configs import Configs
20
+ from proj_flow.log import commit, hosting, rich_text
21
+
22
+ FORCED_LEVEL_CHOICES = list(commit.FORCED_LEVEL.keys())
23
+
24
+
25
+ def _name_list(names: typing.List[str]) -> str:
26
+ if len(names) == 0:
27
+ return ""
28
+
29
+ prefix = ", ".join(names[:-1])
30
+ if prefix:
31
+ prefix += " and "
32
+ return f"{prefix}{names[-1]}"
33
+
34
+
35
+ @arg.command("github")
36
+ def github():
37
+ """Interact with GitHub workflows and releases"""
38
+
39
+
40
+ @arg.command("github", "matrix")
41
+ def matrix(
42
+ official: typing.Annotated[
43
+ bool, arg.FlagArgument(help="Cut matrix to release builds only")
44
+ ],
45
+ rt: env.Runtime,
46
+ ):
47
+ """Supply data for GitHub Actions"""
48
+
49
+ configs = Configs(
50
+ rt,
51
+ argparse.Namespace(configs=[], matrix=True, official=official),
52
+ expand_compilers=False,
53
+ )
54
+
55
+ usable = [usable.items for usable in configs.usable]
56
+ for config in usable:
57
+ if "--orig-compiler" in config:
58
+ orig_compiler = config["--orig-compiler"]
59
+ del config["--orig-compiler"]
60
+ config["compiler"] = orig_compiler
61
+
62
+ if "GITHUB_ACTIONS" in os.environ:
63
+ var = json.dumps({"include": usable})
64
+ GITHUB_OUTPUT = os.environ.get("GITHUB_OUTPUT")
65
+ if GITHUB_OUTPUT is not None:
66
+ with open(GITHUB_OUTPUT, "a", encoding="UTF-8") as github_output:
67
+ print(f"matrix={var}", file=github_output)
68
+ else:
69
+ print(f"matrix={var}")
70
+ else:
71
+ json.dump(usable, sys.stdout)
72
+
73
+
74
+ @arg.command("github", "release")
75
+ def release(
76
+ rt: env.Runtime,
77
+ all: typing.Annotated[
78
+ bool, arg.FlagArgument(help="Take all Conventional Commits.")
79
+ ],
80
+ force: typing.Annotated[
81
+ typing.Optional[str],
82
+ arg.Argument(
83
+ help="Ignore the version change from changelog and instead use this value. "
84
+ f"Allowed values are: {_name_list(FORCED_LEVEL_CHOICES)}",
85
+ meta="level",
86
+ choices=FORCED_LEVEL_CHOICES,
87
+ ),
88
+ ],
89
+ ):
90
+ """
91
+ Bumps the project version based on current git logs, creates a "chore"
92
+ commit for the change, attaches an annotated tag with the version number
93
+ and pushes it all to GitHub.
94
+ """
95
+
96
+ generator = (
97
+ rich_text.api.changelog_generators.first()
98
+ or rich_text.markdown.ChangelogGenerator()
99
+ )
100
+ forced_level = commit.FORCED_LEVEL.get(force) if force else None
101
+ git = commit.Git(rt)
102
+ gh_links = hosting.github.GitHub.from_repo(git) or commit.NoHosting()
103
+
104
+ try:
105
+ log.release.add_release(
106
+ rt=rt,
107
+ forced_level=forced_level,
108
+ take_all=all,
109
+ draft=True,
110
+ generator=generator,
111
+ git=git,
112
+ hosting=gh_links,
113
+ )
114
+ except log.release.VersionNotAdvancing as err:
115
+ rt.message(err.message, level=env.Msg.STATUS)
116
+ return 0
117
+ except log.error.ReleaseError as err:
118
+ rt.fatal(err.message)
@@ -0,0 +1,19 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.github.hosting** adds GitHub hosting environment.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ from proj_flow.log import commit
11
+ from proj_flow.log.hosting.github import GitHub
12
+
13
+
14
+ @commit.hosting_factories.add
15
+ class Plugin(commit.HostingFactory):
16
+ def from_repo(
17
+ self, git: commit.Git, remote: Optional[str] = None
18
+ ) -> Optional[commit.Hosting]:
19
+ return GitHub.from_repo(git, remote)
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.markdown_changelist** .
6
+ """
7
+
8
+ from proj_flow.log.rich_text.api import changelog_generators
9
+ from proj_flow.log.rich_text.markdown import ChangelogGenerator
10
+
11
+
12
+ @changelog_generators.add
13
+ class Plugin(ChangelogGenerator):
14
+ pass
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2025 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ """
5
+ The **proj_flow.ext.python** defines steps and commands for pyproject packages.
6
+ """
7
+
8
+ from . import steps, version
9
+
10
+ __all__ = ["steps", "version"]